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

All: Search: More multi-language support

This commit is contained in:
Laurent Cozic 2019-01-18 17:56:56 +00:00
parent 96cd56548e
commit 8fdc0bf17c
12 changed files with 123 additions and 57 deletions

View File

@ -117,7 +117,6 @@ class Command extends BaseCommand {
this.releaseLockFn_ = null; this.releaseLockFn_ = null;
// Lock is unique per profile/database // Lock is unique per profile/database
// TODO: use SQLite database to do lock?
const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41 const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock'); if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');

View File

@ -40,4 +40,9 @@ fs.readdirSync(guiPath).forEach((filename) => {
} }
}); });
fs.copySync(basePath + '/ReactNativeClient/lib/string-utils-common.js', __dirname + '/gui/note-viewer/lib.js'); const libContent = [
fs.readFileSync(basePath + '/ReactNativeClient/lib/string-utils-common.js', 'utf8'),
fs.readFileSync(basePath + '/ReactNativeClient/lib/markJsUtils.js', 'utf8'),
];
fs.writeFileSync(__dirname + '/gui/note-viewer/lib.js', libContent.join('\n'), 'utf8');

View File

@ -4,6 +4,7 @@ const { connect } = require('react-redux');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const { themeStyle } = require('../theme.js'); const { themeStyle } = require('../theme.js');
const BaseModel = require('lib/BaseModel'); const BaseModel = require('lib/BaseModel');
const markJsUtils = require('lib/markJsUtils');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu; const Menu = bridge().Menu;
@ -14,7 +15,7 @@ const InteropServiceHelper = require('../InteropServiceHelper.js');
const Search = require('lib/models/Search'); const Search = require('lib/models/Search');
const Mark = require('mark.js/dist/mark.min.js'); const Mark = require('mark.js/dist/mark.min.js');
const SearchEngine = require('lib/services/SearchEngine'); const SearchEngine = require('lib/services/SearchEngine');
const { replaceRegexDiacritics } = require('lib/string-utils'); const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils');
class NoteListComponent extends React.Component { class NoteListComponent extends React.Component {
@ -279,15 +280,10 @@ class NoteListComponent extends React.Component {
for (let i = 0; i < highlightedWords.length; i++) { for (let i = 0; i < highlightedWords.length; i++) {
const w = highlightedWords[i]; const w = highlightedWords[i];
if (w.type === 'regex') { markJsUtils.markKeyword(mark, w, {
mark.markRegExp(new RegExp('\\b' + replaceRegexDiacritics(w.value) + '\\b', 'gmi'), { pregQuote: pregQuote,
acrossElements: true, replaceRegexDiacritics: replaceRegexDiacritics,
}); });
} else {
mark.mark([w.value], {
accuracy: 'exactly',
});
}
} }
// Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement // Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement

View File

@ -262,39 +262,46 @@
for (let i = 0; i < keywords.length; i++) { for (let i = 0; i < keywords.length; i++) {
let keyword = keywords[i]; let keyword = keywords[i];
if (typeof keyword === 'string') { markJsUtils.markKeyword(mark_, keyword, {
keyword = { pregQuote: pregQuote,
type: 'text', replaceRegexDiacritics: replaceRegexDiacritics,
value: keyword, }, {
};
}
const isBasicSearch = ['ja', 'zh', 'ko'].indexOf(keyword.scriptType) >= 0;
if (keyword.type === 'regex') {
const b = '[' + pregQuote(' \t\n\r,.,+-*?!={}<>|:"\'()[]') + ']+';
// The capturing groups are a hack to go around the strange behaviour of the ignoreGroups property. What we want is to
// exclude the first and last matches (the boundaries). What ignoreGroups does is ignore the first X groups. So
// we put the first boundary and the keyword inside a group, that way the first groups is ignored (ignoreGroups = 1)
// the second is included. And the last boundary is dropped because it's not in any group (it's important NOT to
// put this one in a group because otherwise it cannot be excluded).
let regexString = '(' + b + ')' + '(' + replaceRegexDiacritics(keyword.value) + ')' + b;
if (isBasicSearch) regexString = keyword.value;
mark_.markRegExp(new RegExp(regexString, 'gmi'), {
each: onEachElement, each: onEachElement,
acrossElements: true,
ignoreGroups: 1,
}); });
} else {
let accuracy = keyword.accuracy ? keyword.accuracy : 'exactly'; // if (typeof keyword === 'string') {
if (isBasicSearch) accuracy = 'partially'; // keyword = {
mark_.mark([keyword.value], { // type: 'text',
each: onEachElement, // value: keyword,
accuracy: accuracy, // };
}); // }
}
// const isBasicSearch = ['ja', 'zh', 'ko'].indexOf(keyword.scriptType) >= 0;
// if (keyword.type === 'regex') {
// const b = '[' + pregQuote(' \t\n\r,.,+-*?!={}<>|:"\'()[]') + ']+';
// // The capturing groups are a hack to go around the strange behaviour of the ignoreGroups property. What we want is to
// // exclude the first and last matches (the boundaries). What ignoreGroups does is ignore the first X groups. So
// // we put the first boundary and the keyword inside a group, that way the first groups is ignored (ignoreGroups = 1)
// // the second is included. And the last boundary is dropped because it's not in any group (it's important NOT to
// // put this one in a group because otherwise it cannot be excluded).
// let regexString = '(' + b + ')' + '(' + replaceRegexDiacritics(keyword.value) + ')' + b;
// if (isBasicSearch) regexString = keyword.value;
// mark_.markRegExp(new RegExp(regexString, 'gmi'), {
// each: onEachElement,
// acrossElements: true,
// ignoreGroups: 1,
// });
// } else {
// let accuracy = keyword.accuracy ? keyword.accuracy : 'exactly';
// if (isBasicSearch) accuracy = 'partially';
// mark_.mark([keyword.value], {
// each: onEachElement,
// accuracy: accuracy,
// });
// }
} }
ipcProxySendToHost('setMarkerCount', elementIndex); ipcProxySendToHost('setMarkerCount', elementIndex);

View File

@ -227,7 +227,7 @@ function themeStyle(theme) {
output = Object.assign({}, globalStyle, fontSizes, darkStyle); output = Object.assign({}, globalStyle, fontSizes, darkStyle);
} }
// TODO: All the theme specific things should go in addExtraStyles // Note: All the theme specific things should go in addExtraStyles
// so that their definition is not split between here and the // so that their definition is not split between here and the
// beginning of the file. At least new styles should go in // beginning of the file. At least new styles should go in
// addExtraStyles. // addExtraStyles.

View File

@ -114,7 +114,7 @@ class NotesScreenComponent extends BaseScreenComponent {
if (props.notesParentType == 'Folder') { if (props.notesParentType == 'Folder') {
notes = await Note.previews(props.selectedFolderId, options); notes = await Note.previews(props.selectedFolderId, options);
} else { } else {
notes = await Tag.notes(props.selectedTagId, options); // TODO: should also return previews notes = await Tag.notes(props.selectedTagId, options);
} }
this.props.dispatch({ this.props.dispatch({

View File

@ -310,7 +310,7 @@ function isImageMimeType(m) {
} }
function addResourceTag(lines, resource, alt = "") { function addResourceTag(lines, resource, alt = "") {
// TODO: refactor to use Resource.markdownTag // Note: refactor to use Resource.markdownTag
let tagAlt = alt == "" ? resource.alt : alt; let tagAlt = alt == "" ? resource.alt : alt;
if (!tagAlt) tagAlt = ''; if (!tagAlt) tagAlt = '';
@ -512,7 +512,6 @@ function enexXmlToMdArray(stream, resources) {
} else if (n == 'q') { } else if (n == 'q') {
section.lines.push('"'); section.lines.push('"');
} else if (n == 'img') { } else if (n == 'img') {
// TODO: TEST IMAGE
if (nodeAttributes.src) { // Many (most?) img tags don't have no source associated, especially when they were imported from HTML if (nodeAttributes.src) { // Many (most?) img tags don't have no source associated, especially when they were imported from HTML
let s = '!['; let s = '![';
if (nodeAttributes.alt) s += nodeAttributes.alt; if (nodeAttributes.alt) s += nodeAttributes.alt;

View File

@ -263,7 +263,7 @@ class JoplinDatabase extends Database {
// must be set in the synchronizer too. // must be set in the synchronizer too.
// Note: v16 and v17 don't do anything. They were used to debug an issue. // Note: v16 and v17 don't do anything. They were used to debug an issue.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]; const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion); let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
@ -474,7 +474,7 @@ class JoplinDatabase extends Database {
END;`); END;`);
} }
if (targetVersion == 16) { if (targetVersion == 18) {
const notesNormalized = ` const notesNormalized = `
CREATE TABLE notes_normalized ( CREATE TABLE notes_normalized (
id TEXT NOT NULL, id TEXT NOT NULL,
@ -520,8 +520,8 @@ class JoplinDatabase extends Database {
try { try {
await this.transactionExecBatch(queries); await this.transactionExecBatch(queries);
} catch (error) { } catch (error) {
if (targetVersion === 15 || targetVersion === 16) { if (targetVersion === 15 || targetVersion === 18) {
this.logger().warn('Could not upgrade to database v15 or v16 - FTS feature will not be used', error); this.logger().warn('Could not upgrade to database v15 or v18 - FTS feature will not be used', error);
} else { } else {
throw error; throw error;
} }

View File

@ -0,0 +1,57 @@
const markJsUtils = {}
markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
if (typeof keyword === 'string') {
keyword = {
type: 'text',
value: keyword,
};
}
const isBasicSearch = ['ja', 'zh', 'ko'].indexOf(keyword.scriptType) >= 0;
let value = keyword.value;
let accuracy = keyword.accuracy ? keyword.accuracy : 'exactly';
if (isBasicSearch) accuracy = 'partially';
if (keyword.type === 'regex') {
accuracy = 'complementary';
value = keyword.originalValue.substr(0, keyword.originalValue.length - 1);
}
mark.mark([value], Object.assign({}, {
accuracy: accuracy,
}, extraOptions));
// if (keyword.type === 'regex') {
// const b = '[' + stringUtils.pregQuote(' \t\n\r,.,+-*?!={}<>|:"\'()[]') + ']+';
// // The capturing groups are a hack to go around the strange behaviour of the ignoreGroups property. What we want is to
// // exclude the first and last matches (the boundaries). What ignoreGroups does is ignore the first X groups. So
// // we put the first boundary and the keyword inside a group, that way the first groups is ignored (ignoreGroups = 1)
// // the second is included. And the last boundary is dropped because it's not in any group (it's important NOT to
// // put this one in a group because otherwise it cannot be excluded).
// let regexString = '(' + b + ')' + '(' + stringUtils.replaceRegexDiacritics(keyword.value) + ')' + b;
// if (isBasicSearch) regexString = keyword.value;
// mark.markRegExp(new RegExp(regexString, 'gmi'), Object.assign({
// acrossElements: true,
// ignoreGroups: 1,
// }, extraOptions));
// } else {
// let accuracy = keyword.accuracy ? keyword.accuracy : 'exactly';
// if (isBasicSearch) accuracy = 'partially';
// mark.mark([keyword.value], Object.assign({}, {
// accuracy: accuracy,
// }, extraOptions));
// }
}
if (typeof module !== 'undefined') {
module.exports = markJsUtils;
}

View File

@ -238,8 +238,6 @@ class Note extends BaseItem {
fields: '*', fields: '*',
} }
// TODO: add support for limits on .search()
let results = await this.previews(folderId, options); let results = await this.previews(folderId, options);
return results.length ? results[0] : null; return results.length ? results[0] : null;
} }

View File

@ -298,9 +298,9 @@ class SearchEngine {
} }
if (term.indexOf('*') >= 0) { if (term.indexOf('*') >= 0) {
terms[col][i] = { type: 'regex', value: this.queryTermToRegex(term), scriptType: scriptType(term) }; terms[col][i] = { type: 'regex', value: this.queryTermToRegex(term), scriptType: scriptType(term), originalValue: term };
} else { } else {
terms[col][i] = { type: 'text', value: term, scriptType: scriptType(term) }; terms[col][i] = { type: 'text', value: term, scriptType: scriptType(term), originalValue: term };
} }
} }
@ -363,9 +363,14 @@ class SearchEngine {
} else { } else {
const parsedQuery = this.parseQuery(query); const parsedQuery = this.parseQuery(query);
const sql = 'SELECT id, title, offsets(notes_fts) AS offsets FROM notes_fts WHERE notes_fts MATCH ?' const sql = 'SELECT id, title, offsets(notes_fts) AS offsets FROM notes_fts WHERE notes_fts MATCH ?'
try {
const rows = await this.db().selectAll(sql, [query]); const rows = await this.db().selectAll(sql, [query]);
this.orderResults_(rows, parsedQuery); this.orderResults_(rows, parsedQuery);
return rows; return rows;
} catch (error) {
this.logger().warn('Cannot execute MATCH query: ' + query + ': ' + error.message);
return [];
}
} }
} }

View File

@ -267,7 +267,7 @@ class Synchronizer {
// This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole // This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole
// content, but for now that will do since being reliable is the priority. // content, but for now that will do since being reliable is the priority.
// //
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe // Note: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test // could expose this with a accurateTimestamps() method that returns "true". In that case, the test
// could be done using the file timestamp and the potentially unnecessary content loading could be skipped. // could be done using the file timestamp and the potentially unnecessary content loading could be skipped.
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be // OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be