1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

All: Resolves #443: Various optimisations to make dealing with large notes easier and make Katex re-rendering faster

This commit is contained in:
Laurent Cozic 2018-05-10 12:02:39 +01:00
parent ef2ffd4e52
commit b9118a90be
5 changed files with 114 additions and 21 deletions

View File

@ -44,4 +44,13 @@ describe('ArrayUtils', function() {
done(); done();
}); });
it('should compare arrays', async (done) => {
expect(ArrayUtils.contentEquals([], [])).toBe(true);
expect(ArrayUtils.contentEquals(['a'], ['a'])).toBe(true);
expect(ArrayUtils.contentEquals(['b', 'a'], ['a', 'b'])).toBe(true);
expect(ArrayUtils.contentEquals(['b'], ['a', 'b'])).toBe(false);
done();
});
}); });

View File

@ -23,6 +23,8 @@ const fs = require('fs-extra');
const {clipboard} = require('electron') const {clipboard} = require('electron')
const md5 = require('md5'); const md5 = require('md5');
const mimeUtils = require('lib/mime-utils.js').mime; const mimeUtils = require('lib/mime-utils.js').mime;
const NoteBodyViewer = require('./NoteBodyViewer.min.js');
const ArrayUtils = require('lib/ArrayUtils');
require('brace/mode/markdown'); require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html // https://ace.c9.io/build/kitchen-sink.html
@ -50,6 +52,7 @@ class NoteTextComponent extends React.Component {
// changed by the user, this variable contains that note ID. Used // changed by the user, this variable contains that note ID. Used
// to automatically set the title. // to automatically set the title.
newAndNoTitleChangeNoteId: null, newAndNoTitleChangeNoteId: null,
bodyHtml: '',
}; };
this.lastLoadedNoteId_ = null; this.lastLoadedNoteId_ = null;
@ -58,6 +61,8 @@ class NoteTextComponent extends React.Component {
this.ignoreNextEditorScroll_ = false; this.ignoreNextEditorScroll_ = false;
this.scheduleSaveTimeout_ = null; this.scheduleSaveTimeout_ = null;
this.restoreScrollTop_ = null; this.restoreScrollTop_ = null;
this.lastSetHtml_ = '';
this.lastSetMarkers_ = [];
// Complicated but reliable method to get editor content height // Complicated but reliable method to get editor content height
// https://github.com/ajaxorg/ace/issues/2046 // https://github.com/ajaxorg/ace/issues/2046
@ -302,7 +307,12 @@ class NoteTextComponent extends React.Component {
newState.newAndNoTitleChangeNoteId = null; newState.newAndNoTitleChangeNoteId = null;
} }
this.lastSetHtml_ = '';
this.lastSetMarkers_ = [];
this.setState(newState); this.setState(newState);
this.updateHtml(newState.note ? newState.note.body : '');
} }
async componentWillReceiveProps(nextProps) { async componentWillReceiveProps(nextProps) {
@ -542,9 +552,52 @@ class NoteTextComponent extends React.Component {
aceEditor_change(body) { aceEditor_change(body) {
shared.noteComponent_change(this, 'body', body); shared.noteComponent_change(this, 'body', body);
this.scheduleHtmlUpdate();
this.scheduleSave(); this.scheduleSave();
} }
scheduleHtmlUpdate(timeout = 500) {
if (this.scheduleHtmlUpdateIID_) {
clearTimeout(this.scheduleHtmlUpdateIID_);
this.scheduleHtmlUpdateIID_ = null;
}
if (timeout) {
this.scheduleHtmlUpdateIID_ = setTimeout(() => {
this.updateHtml();
}, timeout);
} else {
this.updateHtml();
}
}
updateHtml(body = null) {
const mdOptions = {
onResourceLoaded: () => {
this.updateHtml();
this.forceUpdate();
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
const theme = themeStyle(this.props.theme);
let bodyToRender = body;
if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
let bodyHtml = '';
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) {
// Fixes https://github.com/laurent22/joplin/issues/217
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*';
}
bodyHtml = this.mdToHtml().render(bodyToRender, theme, mdOptions);
this.setState({ bodyHtml: bodyHtml });
}
async doCommand(command) { async doCommand(command) {
if (!command) return; if (!command) return;
@ -601,6 +654,8 @@ class NoteTextComponent extends React.Component {
note: Object.assign({}, note), note: Object.assign({}, note),
lastSavedNote: Object.assign({}, note), lastSavedNote: Object.assign({}, note),
}); });
this.updateHtml(note.body);
} catch (error) { } catch (error) {
reg.logger().error(error); reg.logger().error(error);
bridge().showErrorMessageBox(error.message); bridge().showErrorMessageBox(error.message);
@ -748,25 +803,21 @@ class NoteTextComponent extends React.Component {
} }
if (this.state.webviewReady) { if (this.state.webviewReady) {
const mdOptions = { let html = this.state.bodyHtml;
onResourceLoaded: () => {
this.forceUpdate();
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
let bodyToRender = body; const htmlHasChanged = this.lastSetHtml_ !== html;
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) { if (htmlHasChanged) {
// Fixes https://github.com/laurent22/joplin/issues/217 this.webview_.send('setHtml', html);
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*'; this.lastSetHtml_ = html;
} }
const html = this.mdToHtml().render(bodyToRender, theme, mdOptions);
this.webview_.send('setHtml', html);
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
const keywords = search ? Search.keywords(search.query_pattern) : []; const keywords = search ? Search.keywords(search.query_pattern) : [];
this.webview_.send('setMarkers', keywords);
if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords)) {
this.lastSetMarkers_ = [];
this.webview_.send('setMarkers', keywords);
}
} }
const toolbarItems = []; const toolbarItems = [];
@ -812,7 +863,7 @@ class NoteTextComponent extends React.Component {
const titleBarDate = <span style={Object.assign({}, theme.textStyle, {color: theme.colorFaded})}>{time.formatMsToLocal(note.user_updated_time)}</span> const titleBarDate = <span style={Object.assign({}, theme.textStyle, {color: theme.colorFaded})}>{time.formatMsToLocal(note.user_updated_time)}</span>
const viewer = <webview const viewer = <webview
style={viewerStyle} style={viewerStyle}
nodeintegration="1" nodeintegration="1"
src="gui/note-viewer/index.html" src="gui/note-viewer/index.html"

View File

@ -45,4 +45,17 @@ ArrayUtils.findByKey = function(array, key, value) {
return null; return null;
} }
ArrayUtils.contentEquals = function(array1, array2) {
if (array1 === array2) return true;
if (!array1.length && !array2.length) return true;
if (array1.length !== array2.length) return false;
for (let i = 0; i < array1.length; i++) {
const a1 = array1[i];
if (array2.indexOf(a1) < 0) return false;
}
return true;
}
module.exports = ArrayUtils; module.exports = ArrayUtils;

View File

@ -157,9 +157,12 @@ class MdToHtml {
rendererPlugin_(language) { rendererPlugin_(language) {
if (!language) return null; if (!language) return null;
const handlers = {}; if (!this.rendererPlugins_) {
handlers['katex'] = new MdToHtml_Katex(); this.rendererPlugins_ = {};
return language in handlers ? handlers[language] : null; this.rendererPlugins_['katex'] = new MdToHtml_Katex();
}
return language in this.rendererPlugins_ ? this.rendererPlugins_[language] : null;
} }
parseInlineCodeLanguage_(content) { parseInlineCodeLanguage_(content) {

View File

@ -5,15 +5,28 @@ const Setting = require('lib/models/Setting');
class MdToHtml_Katex { class MdToHtml_Katex {
constructor() {
this.cache_ = {};
this.assetsLoaded_ = false;
}
name() { name() {
return 'katex'; return 'katex';
} }
processContent(renderedTokens, content, tagType) { processContent(renderedTokens, content, tagType) {
try { try {
let renderered = katex.renderToString(content, { const cacheKey = tagType + '_' + content;
displayMode: tagType === 'block', let renderered = null;
});
if (this.cache_[cacheKey]) {
renderered = this.cache_[cacheKey];
} else {
renderered = katex.renderToString(content, {
displayMode: tagType === 'block',
});
this.cache_[cacheKey] = renderered;
}
if (tagType === 'block') renderered = '<p>' + renderered + '</p>'; if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
@ -29,6 +42,8 @@ class MdToHtml_Katex {
} }
async loadAssets() { async loadAssets() {
if (this.assetsLoaded_) return;
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts // In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed // In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
@ -43,6 +58,8 @@ class MdToHtml_Katex {
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' }); await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' }); await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
} }
this.assetsLoaded_ = true;
} }
} }