You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-13 22:12:50 +02:00
Merge branch 'master' of github.com:laurent22/joplin
This commit is contained in:
4
BUILD.md
4
BUILD.md
@@ -34,8 +34,8 @@ First you need to setup React Native to build projects with native code. For thi
|
|||||||
Then:
|
Then:
|
||||||
|
|
||||||
cd ReactNativeClient
|
cd ReactNativeClient
|
||||||
npm start-android
|
npm run start-android
|
||||||
# Or: npm start-ios
|
# Or: npm run start-ios
|
||||||
|
|
||||||
To run the iOS application, it might be easier to open the file `ios/Joplin.xcworkspace` on XCode and run the app from there.
|
To run the iOS application, it might be easier to open the file `ios/Joplin.xcworkspace` on XCode and run the app from there.
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
|
|||||||
const { sortedIds, createNTestNotes, asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
const { sortedIds, createNTestNotes, asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||||
const Folder = require('lib/models/Folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
const BaseModel = require('lib/BaseModel.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||||
const { shim } = require('lib/shim');
|
const { shim } = require('lib/shim');
|
||||||
@@ -216,4 +217,54 @@ describe('models_Note', function() {
|
|||||||
const hasThrown = await checkThrowAsync(async () => await Folder.copyToFolder(note1.id, folder2.id));
|
const hasThrown = await checkThrowAsync(async () => await Folder.copyToFolder(note1.id, folder2.id));
|
||||||
expect(hasThrown).toBe(true);
|
expect(hasThrown).toBe(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should convert resource paths from internal to external paths', asyncTest(async () => {
|
||||||
|
const resourceDirName = Setting.value('resourceDirName');
|
||||||
|
const resourceDir = Setting.value('resourceDir');
|
||||||
|
const r1 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`);
|
||||||
|
const r2 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`);
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
true,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
``,
|
||||||
|
``,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
`  `,
|
||||||
|
`  `,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
true,
|
||||||
|
``,
|
||||||
|
``,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
true,
|
||||||
|
`  `,
|
||||||
|
`  `,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const [useAbsolutePaths, input, expected] = testCase;
|
||||||
|
const internalToExternal = await Note.replaceResourceInternalToExternalLinks(input, { useAbsolutePaths });
|
||||||
|
expect(expected).toBe(internalToExternal);
|
||||||
|
|
||||||
|
const externalToInternal = await Note.replaceResourceExternalToInternalLinks(internalToExternal, { useAbsolutePaths });
|
||||||
|
expect(externalToInternal).toBe(input);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -92,6 +92,32 @@ describe('services_SearchEngine', function() {
|
|||||||
expect(rows[2].id).toBe(n1.id);
|
expect(rows[2].id).toBe(n1.id);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should tell where the results are found', asyncTest(async () => {
|
||||||
|
const notes = [
|
||||||
|
await Note.save({ title: 'abcd efgh', body: 'abcd' }),
|
||||||
|
await Note.save({ title: 'abcd' }),
|
||||||
|
await Note.save({ title: 'efgh', body: 'abcd' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
await engine.syncTables();
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
['abcd', ['title', 'body'], ['title'], ['body']],
|
||||||
|
['efgh', ['title'], [], ['title']],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const rows = await engine.search(testCase[0]);
|
||||||
|
|
||||||
|
for (let i = 0; i < notes.length; i++) {
|
||||||
|
const row = rows.find(row => row.id === notes[i].id);
|
||||||
|
const actual = row ? row.fields.sort().join(',') : '';
|
||||||
|
const expected = testCase[i + 1].sort().join(',');
|
||||||
|
expect(expected).toBe(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
it('should order search results by relevance (2)', asyncTest(async () => {
|
it('should order search results by relevance (2)', asyncTest(async () => {
|
||||||
// 1
|
// 1
|
||||||
const n1 = await Note.save({ title: 'abcd efgh', body: 'XX abcd XX efgh' });
|
const n1 = await Note.save({ title: 'abcd efgh', body: 'XX abcd XX efgh' });
|
||||||
|
@@ -143,6 +143,7 @@ async function switchClient(id) {
|
|||||||
Resource.encryptionService_ = encryptionServices_[id];
|
Resource.encryptionService_ = encryptionServices_[id];
|
||||||
BaseItem.revisionService_ = revisionServices_[id];
|
BaseItem.revisionService_ = revisionServices_[id];
|
||||||
|
|
||||||
|
Setting.setConstant('resourceDirName', resourceDirName(id));
|
||||||
Setting.setConstant('resourceDir', resourceDir(id));
|
Setting.setConstant('resourceDir', resourceDir(id));
|
||||||
|
|
||||||
await Setting.load();
|
await Setting.load();
|
||||||
@@ -213,9 +214,14 @@ async function setupDatabase(id = null) {
|
|||||||
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
|
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resourceDirName(id = null) {
|
||||||
|
if (id === null) id = currentClient_;
|
||||||
|
return `resources-${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
function resourceDir(id = null) {
|
function resourceDir(id = null) {
|
||||||
if (id === null) id = currentClient_;
|
if (id === null) id = currentClient_;
|
||||||
return `${__dirname}/data/resources-${id}`;
|
return `${__dirname}/data/${resourceDirName(id)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDatabaseAndSynchronizer(id = null) {
|
async function setupDatabaseAndSynchronizer(id = null) {
|
||||||
|
@@ -52,8 +52,14 @@ class InteropServiceHelper {
|
|||||||
win = bridge().newBrowserWindow(windowOptions);
|
win = bridge().newBrowserWindow(windowOptions);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
win.webContents.on('did-finish-load', async () => {
|
win.webContents.on('did-finish-load', () => {
|
||||||
|
|
||||||
|
// did-finish-load will trigger when most assets are done loading, probably
|
||||||
|
// images, JavaScript and CSS. However it seems it might trigger *before*
|
||||||
|
// all fonts are loaded, which will break for example Katex rendering.
|
||||||
|
// So we need to add an additional timer to make sure fonts are loaded
|
||||||
|
// as it doesn't seem there's any easy way to figure that out.
|
||||||
|
setTimeout(async () => {
|
||||||
if (target === 'pdf') {
|
if (target === 'pdf') {
|
||||||
try {
|
try {
|
||||||
const data = await win.webContents.printToPDF(options);
|
const data = await win.webContents.printToPDF(options);
|
||||||
@@ -73,6 +79,8 @@ class InteropServiceHelper {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
win.loadURL(url.format({
|
win.loadURL(url.format({
|
||||||
|
@@ -233,7 +233,7 @@ class PromptDialog extends React.Component {
|
|||||||
const buttonComps = [];
|
const buttonComps = [];
|
||||||
if (buttonTypes.indexOf('ok') >= 0) {
|
if (buttonTypes.indexOf('ok') >= 0) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button key="ok" style={styles.button} onClick={() => onClose(true, 'ok')}>
|
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>
|
||||||
{_('OK')}
|
{_('OK')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@@ -55,9 +55,10 @@ function findBlockSource(node:any) {
|
|||||||
|
|
||||||
function newBlockSource(language:string = '', content:string = ''):any {
|
function newBlockSource(language:string = '', content:string = ''):any {
|
||||||
const fence = language === 'katex' ? '$$' : '```';
|
const fence = language === 'katex' ? '$$' : '```';
|
||||||
|
const fenceLanguage = language === 'katex' ? '' : language;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openCharacters: `\n${fence}${language}\n`,
|
openCharacters: `\n${fence}${fenceLanguage}\n`,
|
||||||
closeCharacters: `\n${fence}\n`,
|
closeCharacters: `\n${fence}\n`,
|
||||||
content: content,
|
content: content,
|
||||||
node: null,
|
node: null,
|
||||||
@@ -444,6 +445,7 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
|
|||||||
noneditable_noneditable_class: 'joplin-editable', // Can be a regex too
|
noneditable_noneditable_class: 'joplin-editable', // Can be a regex too
|
||||||
valid_elements: '*[*]', // We already filter in sanitize_html
|
valid_elements: '*[*]', // We already filter in sanitize_html
|
||||||
menubar: false,
|
menubar: false,
|
||||||
|
relative_urls: false,
|
||||||
branding: false,
|
branding: false,
|
||||||
target_list: false,
|
target_list: false,
|
||||||
table_resize_bars: false,
|
table_resize_bars: false,
|
||||||
|
@@ -12,7 +12,6 @@ const HelpButton = require('../gui/HelpButton.min');
|
|||||||
const { surroundKeywords, nextWhitespaceIndex } = require('lib/string-utils.js');
|
const { surroundKeywords, nextWhitespaceIndex } = require('lib/string-utils.js');
|
||||||
const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js');
|
const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js');
|
||||||
const PLUGIN_NAME = 'gotoAnything';
|
const PLUGIN_NAME = 'gotoAnything';
|
||||||
const itemHeight = 60;
|
|
||||||
|
|
||||||
class GotoAnything {
|
class GotoAnything {
|
||||||
|
|
||||||
@@ -38,6 +37,7 @@ class Dialog extends React.PureComponent {
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
listType: BaseModel.TYPE_NOTE,
|
listType: BaseModel.TYPE_NOTE,
|
||||||
showHelp: false,
|
showHelp: false,
|
||||||
|
resultsInBody: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
@@ -55,14 +55,30 @@ class Dialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
style() {
|
style() {
|
||||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
const styleKey = [this.props.theme, this.state.resultsInBody ? '1' : '0'].join('-');
|
||||||
|
|
||||||
|
if (this.styles_[styleKey]) return this.styles_[styleKey];
|
||||||
|
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
this.styles_[this.props.theme] = {
|
const itemHeight = this.state.resultsInBody ? 84 : 64;
|
||||||
|
|
||||||
|
this.styles_[styleKey] = {
|
||||||
dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }),
|
dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }),
|
||||||
input: Object.assign({}, theme.inputStyle, { flex: 1 }),
|
input: Object.assign({}, theme.inputStyle, { flex: 1 }),
|
||||||
row: { overflow: 'hidden', height: itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10 },
|
row: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: itemHeight,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
borderBottomColor: theme.dividerColor,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
help: Object.assign({}, theme.textStyle, { marginBottom: 10 }),
|
help: Object.assign({}, theme.textStyle, { marginBottom: 10 }),
|
||||||
inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
|
inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
|
||||||
};
|
};
|
||||||
@@ -78,22 +94,23 @@ class Dialog extends React.PureComponent {
|
|||||||
|
|
||||||
const rowTitleStyle = Object.assign({}, rowTextStyle, {
|
const rowTitleStyle = Object.assign({}, rowTextStyle, {
|
||||||
fontSize: rowTextStyle.fontSize * 1.4,
|
fontSize: rowTextStyle.fontSize * 1.4,
|
||||||
marginBottom: 4,
|
marginBottom: this.state.resultsInBody ? 6 : 4,
|
||||||
color: theme.colorFaded,
|
color: theme.colorFaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowFragmentsStyle = Object.assign({}, rowTextStyle, {
|
const rowFragmentsStyle = Object.assign({}, rowTextStyle, {
|
||||||
fontSize: rowTextStyle.fontSize * 1.2,
|
fontSize: rowTextStyle.fontSize * 1.2,
|
||||||
marginBottom: 4,
|
marginBottom: this.state.resultsInBody ? 8 : 6,
|
||||||
color: theme.colorFaded,
|
color: theme.colorFaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.styles_[this.props.theme].rowSelected = Object.assign({}, this.styles_[this.props.theme].row, { backgroundColor: theme.selectedColor });
|
this.styles_[styleKey].rowSelected = Object.assign({}, this.styles_[styleKey].row, { backgroundColor: theme.selectedColor });
|
||||||
this.styles_[this.props.theme].rowPath = rowTextStyle;
|
this.styles_[styleKey].rowPath = rowTextStyle;
|
||||||
this.styles_[this.props.theme].rowTitle = rowTitleStyle;
|
this.styles_[styleKey].rowTitle = rowTitleStyle;
|
||||||
this.styles_[this.props.theme].rowFragments = rowFragmentsStyle;
|
this.styles_[styleKey].rowFragments = rowFragmentsStyle;
|
||||||
|
this.styles_[styleKey].itemHeight = itemHeight;
|
||||||
|
|
||||||
return this.styles_[this.props.theme];
|
return this.styles_[styleKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -144,17 +161,14 @@ class Dialog extends React.PureComponent {
|
|||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
makeSearchQuery(query, field) {
|
makeSearchQuery(query) {
|
||||||
const output = [];
|
const output = [];
|
||||||
const splitted = (field === 'title')
|
const splitted = query.split(' ');
|
||||||
? query.split(' ')
|
|
||||||
: query.substr(1).trim().split(' '); // body
|
|
||||||
|
|
||||||
for (let i = 0; i < splitted.length; i++) {
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
const s = splitted[i].trim();
|
const s = splitted[i].trim();
|
||||||
if (!s) continue;
|
if (!s) continue;
|
||||||
|
output.push(`${s}*`);
|
||||||
output.push(field === 'title' ? `title:${s}*` : `body:${s}*`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.join(' ');
|
return output.join(' ');
|
||||||
@@ -166,6 +180,8 @@ class Dialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateList() {
|
async updateList() {
|
||||||
|
let resultsInBody = false;
|
||||||
|
|
||||||
if (!this.state.query) {
|
if (!this.state.query) {
|
||||||
this.setState({ results: [], keywords: [] });
|
this.setState({ results: [], keywords: [] });
|
||||||
} else {
|
} else {
|
||||||
@@ -187,11 +203,20 @@ class Dialog extends React.PureComponent {
|
|||||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||||
results[i] = Object.assign({}, row, { path: path ? path : '/' });
|
results[i] = Object.assign({}, row, { path: path ? path : '/' });
|
||||||
}
|
}
|
||||||
} else if (this.state.query.indexOf('/') === 0) { // BODY
|
} else { // Note TITLE or BODY
|
||||||
listType = BaseModel.TYPE_NOTE;
|
listType = BaseModel.TYPE_NOTE;
|
||||||
searchQuery = this.makeSearchQuery(this.state.query, 'body');
|
searchQuery = this.makeSearchQuery(this.state.query);
|
||||||
results = await SearchEngine.instance().search(searchQuery);
|
results = await SearchEngine.instance().search(searchQuery);
|
||||||
|
|
||||||
|
resultsInBody = !!results.find(row => row.fields.includes('body'));
|
||||||
|
|
||||||
|
if (!resultsInBody) {
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const row = results[i];
|
||||||
|
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||||
|
results[i] = Object.assign({}, row, { path: path });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
const searchKeywords = this.keywords(searchQuery);
|
const searchKeywords = this.keywords(searchQuery);
|
||||||
const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body'] });
|
const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body'] });
|
||||||
@@ -199,6 +224,9 @@ class Dialog extends React.PureComponent {
|
|||||||
|
|
||||||
for (let i = 0; i < results.length; i++) {
|
for (let i = 0; i < results.length; i++) {
|
||||||
const row = results[i];
|
const row = results[i];
|
||||||
|
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||||
|
|
||||||
|
if (row.fields.includes('body')) {
|
||||||
let fragments = '...';
|
let fragments = '...';
|
||||||
|
|
||||||
if (i < limit) { // Display note fragments of search keyword matches
|
if (i < limit) { // Display note fragments of search keyword matches
|
||||||
@@ -224,18 +252,11 @@ class Dialog extends React.PureComponent {
|
|||||||
if (mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
|
if (mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
|
||||||
results[i] = Object.assign({}, row, { path, fragments });
|
results[i] = Object.assign({}, row, { path, fragments });
|
||||||
|
} else {
|
||||||
|
results[i] = Object.assign({}, row, { path: path, fragments: '' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else { // TITLE
|
|
||||||
listType = BaseModel.TYPE_NOTE;
|
|
||||||
searchQuery = this.makeSearchQuery(this.state.query, 'title');
|
|
||||||
results = await SearchEngine.instance().search(searchQuery);
|
|
||||||
|
|
||||||
for (let i = 0; i < results.length; i++) {
|
|
||||||
const row = results[i];
|
|
||||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
|
||||||
results[i] = Object.assign({}, row, { path: path });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +273,7 @@ class Dialog extends React.PureComponent {
|
|||||||
results: results,
|
results: results,
|
||||||
keywords: this.keywords(searchQuery),
|
keywords: this.keywords(searchQuery),
|
||||||
selectedItemId: selectedItemId,
|
selectedItemId: selectedItemId,
|
||||||
|
resultsInBody: resultsInBody,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,12 +337,15 @@ class Dialog extends React.PureComponent {
|
|||||||
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>');
|
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>');
|
||||||
|
|
||||||
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>');
|
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>');
|
||||||
const pathComp = !item.path ? null : <div style={style.rowPath}>{item.path}</div>;
|
|
||||||
|
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" />;
|
||||||
|
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
||||||
|
const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: fragmentsHtml }}></div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}>
|
<div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}>
|
||||||
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
|
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
|
||||||
<div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: fragmentsHtml }}></div>
|
{fragmentComp}
|
||||||
{pathComp}
|
{pathComp}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -374,17 +399,19 @@ class Dialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderList() {
|
renderList() {
|
||||||
const style = {
|
const style = this.style();
|
||||||
|
|
||||||
|
const itemListStyle = {
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
height: Math.min(itemHeight * this.state.results.length, 7 * itemHeight),
|
height: Math.min(style.itemHeight * this.state.results.length, 7 * style.itemHeight),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemList
|
<ItemList
|
||||||
ref={this.itemListRef}
|
ref={this.itemListRef}
|
||||||
itemHeight={itemHeight}
|
itemHeight={style.itemHeight}
|
||||||
items={this.state.results}
|
items={this.state.results}
|
||||||
style={style}
|
style={itemListStyle}
|
||||||
itemRenderer={this.listItemRenderer}
|
itemRenderer={this.listItemRenderer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -393,7 +420,7 @@ class Dialog extends React.PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
const style = this.style();
|
const style = this.style();
|
||||||
const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title to jump to it. Or type # followed by a tag name, or @ followed by a notebook name, or / followed by note content.')}</div>;
|
const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={this.modalLayer_onClick} style={theme.dialogModalLayer}>
|
<div onClick={this.modalLayer_onClick} style={theme.dialogModalLayer}>
|
||||||
|
@@ -301,7 +301,7 @@ Notes are sorted by "relevance". Currently it means the notes that contain the r
|
|||||||
|
|
||||||
# Goto Anything
|
# Goto Anything
|
||||||
|
|
||||||
In the desktop application, press Ctrl+G or Cmd+G and type the title of a note to jump directly to it. You can also type `#` followed by a tag or `@` followed by a notebook title.
|
In the desktop application, press <kbd>Ctrl+G</kbd> or <kbd>Cmd+G</kbd> and type a note title or part of its content to jump to it. Or type <kbd>#</kbd> followed by a tag name, or <kbd>@</kbd> followed by a notebook name.
|
||||||
|
|
||||||
# Global shortcut
|
# Global shortcut
|
||||||
|
|
||||||
|
@@ -124,6 +124,7 @@ class JoplinDatabase extends Database {
|
|||||||
this.initialized_ = false;
|
this.initialized_ = false;
|
||||||
this.tableFields_ = null;
|
this.tableFields_ = null;
|
||||||
this.version_ = null;
|
this.version_ = null;
|
||||||
|
this.tableFieldNames_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized() {
|
initialized() {
|
||||||
@@ -136,11 +137,14 @@ class JoplinDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableFieldNames(tableName) {
|
tableFieldNames(tableName) {
|
||||||
|
if (this.tableFieldNames_[tableName]) return this.tableFieldNames_[tableName];
|
||||||
|
|
||||||
const tf = this.tableFields(tableName);
|
const tf = this.tableFields(tableName);
|
||||||
const output = [];
|
const output = [];
|
||||||
for (let i = 0; i < tf.length; i++) {
|
for (let i = 0; i < tf.length; i++) {
|
||||||
output.push(tf[i].name);
|
output.push(tf[i].name);
|
||||||
}
|
}
|
||||||
|
this.tableFieldNames_[tableName] = output;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -230,7 +230,7 @@ module.exports = {
|
|||||||
const katexInline = function(latex) {
|
const katexInline = function(latex) {
|
||||||
options.displayMode = false;
|
options.displayMode = false;
|
||||||
try {
|
try {
|
||||||
return `<span class="joplin-editable"><span class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$" data-joplin-source-close="$">${latex}</span>${renderToStringWithCache(latex, options)}</span>`;
|
return `<span class="joplin-editable"><span class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$" data-joplin-source-close="$">${md.utils.escapeHtml(latex)}</span>${renderToStringWithCache(latex, options)}</span>`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Katex error for:', latex, error);
|
console.error('Katex error for:', latex, error);
|
||||||
return latex;
|
return latex;
|
||||||
@@ -245,7 +245,7 @@ module.exports = {
|
|||||||
const katexBlock = function(latex) {
|
const katexBlock = function(latex) {
|
||||||
options.displayMode = true;
|
options.displayMode = true;
|
||||||
try {
|
try {
|
||||||
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$ " data-joplin-source-close=" $$ ">${latex}</pre>${renderToStringWithCache(latex, options)}</div>`;
|
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$ " data-joplin-source-close=" $$ ">${md.utils.escapeHtml(latex)}</pre>${renderToStringWithCache(latex, options)}</div>`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Katex error for:', latex, error);
|
console.error('Katex error for:', latex, error);
|
||||||
return latex;
|
return latex;
|
||||||
|
@@ -162,10 +162,12 @@ class Note extends BaseItem {
|
|||||||
const id = resourceIds[i];
|
const id = resourceIds[i];
|
||||||
const resource = await Resource.load(id);
|
const resource = await Resource.load(id);
|
||||||
if (!resource) continue;
|
if (!resource) continue;
|
||||||
const resourcePath = options.useAbsolutePaths ? Resource.fullPath(resource) : Resource.relativePath(resource);
|
const resourcePath = options.useAbsolutePaths ? `file://${Resource.fullPath(resource)}` : Resource.relativePath(resource);
|
||||||
body = body.replace(new RegExp(`:/${id}`, 'gi'), resourcePath);
|
body = body.replace(new RegExp(`:/${id}`, 'gi'), resourcePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger().info('replaceResourceInternalToExternalLinks result', body);
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +178,8 @@ class Note extends BaseItem {
|
|||||||
|
|
||||||
const pathsToTry = [];
|
const pathsToTry = [];
|
||||||
if (options.useAbsolutePaths) {
|
if (options.useAbsolutePaths) {
|
||||||
pathsToTry.push(Setting.value('resourceDir'));
|
pathsToTry.push(`file://${Setting.value('resourceDir')}`);
|
||||||
pathsToTry.push(shim.pathRelativeToCwd(Setting.value('resourceDir')));
|
pathsToTry.push(`file://${shim.pathRelativeToCwd(Setting.value('resourceDir'))}`);
|
||||||
} else {
|
} else {
|
||||||
pathsToTry.push(Resource.baseRelativeDirectoryPath());
|
pathsToTry.push(Resource.baseRelativeDirectoryPath());
|
||||||
}
|
}
|
||||||
@@ -193,6 +195,8 @@ class Note extends BaseItem {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger().info('replaceResourceExternalToInternalLinks result', body);
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -192,16 +192,17 @@ class SearchEngine {
|
|||||||
return row && row['total'] ? row['total'] : 0;
|
return row && row['total'] ? row['total'] : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
columnIndexesFromOffsets_(offsets) {
|
fieldNamesFromOffsets_(offsets) {
|
||||||
|
const notesNormalizedFieldNames = this.db().tableFieldNames('notes_normalized');
|
||||||
const occurenceCount = Math.floor(offsets.length / 4);
|
const occurenceCount = Math.floor(offsets.length / 4);
|
||||||
const indexes = [];
|
const output = [];
|
||||||
|
|
||||||
for (let i = 0; i < occurenceCount; i++) {
|
for (let i = 0; i < occurenceCount; i++) {
|
||||||
const colIndex = offsets[i * 4] - 1;
|
const colIndex = offsets[i * 4];
|
||||||
if (indexes.indexOf(colIndex) < 0) indexes.push(colIndex);
|
const fieldName = notesNormalizedFieldNames[colIndex];
|
||||||
|
if (!output.includes(fieldName)) output.push(fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexes;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateWeight_(offsets, termCount) {
|
calculateWeight_(offsets, termCount) {
|
||||||
@@ -234,16 +235,17 @@ class SearchEngine {
|
|||||||
return occurenceCount / spread;
|
return occurenceCount / spread;
|
||||||
}
|
}
|
||||||
|
|
||||||
orderResults_(rows, parsedQuery) {
|
processResults_(rows, parsedQuery) {
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const row = rows[i];
|
const row = rows[i];
|
||||||
const offsets = row.offsets.split(' ').map(o => Number(o));
|
const offsets = row.offsets.split(' ').map(o => Number(o));
|
||||||
row.weight = this.calculateWeight_(offsets, parsedQuery.termCount);
|
row.weight = this.calculateWeight_(offsets, parsedQuery.termCount);
|
||||||
// row.colIndexes = this.columnIndexesFromOffsets_(offsets);
|
row.fields = this.fieldNamesFromOffsets_(offsets);
|
||||||
// row.offsets = offsets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.sort((a, b) => {
|
rows.sort((a, b) => {
|
||||||
|
if (a.fields.includes('title') && !b.fields.includes('title')) return -1;
|
||||||
|
if (!a.fields.includes('title') && b.fields.includes('title')) return +1;
|
||||||
if (a.weight < b.weight) return +1;
|
if (a.weight < b.weight) return +1;
|
||||||
if (a.weight > b.weight) return -1;
|
if (a.weight > b.weight) return -1;
|
||||||
if (a.is_todo && a.todo_completed) return +1;
|
if (a.is_todo && a.todo_completed) return +1;
|
||||||
@@ -404,7 +406,7 @@ class SearchEngine {
|
|||||||
const sql = 'SELECT notes_fts.id, notes_fts.title AS normalized_title, offsets(notes_fts) AS offsets, notes.title, notes.user_updated_time, notes.is_todo, notes.todo_completed, notes.parent_id FROM notes_fts LEFT JOIN notes ON notes_fts.id = notes.id WHERE notes_fts MATCH ?';
|
const sql = 'SELECT notes_fts.id, notes_fts.title AS normalized_title, offsets(notes_fts) AS offsets, notes.title, notes.user_updated_time, notes.is_todo, notes.todo_completed, notes.parent_id FROM notes_fts LEFT JOIN notes ON notes_fts.id = notes.id WHERE notes_fts MATCH ?';
|
||||||
try {
|
try {
|
||||||
const rows = await this.db().selectAll(sql, [query]);
|
const rows = await this.db().selectAll(sql, [query]);
|
||||||
this.orderResults_(rows, parsedQuery);
|
this.processResults_(rows, parsedQuery);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger().warn(`Cannot execute MATCH query: ${query}: ${error.message}`);
|
this.logger().warn(`Cannot execute MATCH query: ${query}: ${error.message}`);
|
||||||
|
@@ -752,7 +752,7 @@ class AppComponent extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatusBar barStyle="dark-content" />
|
<StatusBar barStyle="dark-content" />
|
||||||
<MenuContext style={{ flex: 1 }}>
|
<MenuContext style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||||
<AppNav screens={appNavInit} />
|
<AppNav screens={appNavInit} />
|
||||||
|
Reference in New Issue
Block a user