1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-05 20:56:22 +02:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Laurent Cozic
acc10ccac4 Electron release v1.0.221 2020-06-20 02:31:12 +01:00
Laurent Cozic
45160a2e73 Desktop: API: Add support for external editing from API 2020-06-20 02:30:09 +01:00
Laurent Cozic
e38794171a Tools: Fix desktop build issue on Windows 2020-06-20 01:18:46 +01:00
Laurent Cozic
c2d6da83c0 All: Fixes #3381: Prevent desktop.ini file from breaking sync lock 2020-06-20 01:15:56 +01:00
Caleb John
77005fc495 Tools: Try to add codemirror types again (#3380)
* Try add codemirror type again

* Add global typedef for codemirror

* Add comment
2020-06-18 18:58:11 +01:00
Deep Majumder
0273e58783 Doc: Corrected info on supported Linux dsitros. (#3378)
Added list of supported desktop environments
2020-06-18 18:55:41 +01:00
Laurent Cozic
3e88a24753 Desktop: Reduce database logging statements 2020-06-17 23:46:47 +01:00
Laurent Cozic
1205908233 Revert "Desktop: Change codemirror to user import syntax (#3375)"
This reverts commit 3a7a068196.
2020-06-17 23:45:36 +01:00
Caleb John
3a7a068196 Desktop: Change codemirror to user import syntax (#3375) 2020-06-17 18:47:11 +01:00
Caleb John
845ecfe742 Desktop: Add default filename for jex export (#3034)
* Add default filename for jex export

* Restructure date as default filename
2020-06-17 18:35:13 +01:00
Laurent Cozic
53355aaad3 Clipper: Do not export invisible elements 2020-06-17 00:03:40 +01:00
Laurent Cozic
7cd0e25538 Merge branch 'master' of github.com:laurent22/joplin 2020-06-16 18:10:04 +01:00
Caleb John
2f15178ff6 Desktop: Remove auto-indent for in note html/xml for Code Mirror (#3374)
html/xml inside of a codeblock will still benefit from auto-indent
2020-06-16 13:00:58 +01:00
Caleb John
a4b13be0d1 Desktop: Fix getLineSpan logic and list token regex logic (#3365)
- previously getLineSpan was included line text as a token (dumb
oversight)
- the regex was updated to include a space after a OL element (was
missing for some reason)
2020-06-16 13:00:17 +01:00
Caleb John
23ae4fb790 Desktop: Add swapLineUp and swapLineDown keys to Code Mirror editor (#3363) 2020-06-16 12:59:33 +01:00
Laurent Cozic
cff036d08b Desktop: Do not expand the leaft notebook when following a link to a note 2020-06-15 22:59:42 +00:00
Laurent Cozic
7826cc0b03 Doc: Added sponsor 2020-06-15 23:03:09 +01:00
Alexander van der Berg
2bfa28a311 Doc: Fix sponsors table layout in the GitHub README.md (#3362)
The table was displayed correctly only on https://joplinapp.org/
2020-06-15 23:00:50 +01:00
Laurent Cozic
780c5c80ae Revert "Desktop: Fixes #3302: Prevent drag and dropping of text in editor as it can lead to data loss"
This reverts commit a75db94da5.

It turns out people often drag and drop text, so restoring the functionality.

See https://discourse.joplinapp.org/t/9406
2020-06-15 19:16:22 +01:00
Laurent Cozic
fcd00b3212 Desktop, Cli: Fixed various bugs related to the import of ENEX files as HTML 2020-06-15 17:10:51 +01:00
Laurent Cozic
6a41d6e85a Cli: Allow importing ENEX files as HTML 2020-06-14 16:45:17 +00:00
Laurent Cozic
3733858145 Desktop, Cli: Fixed issue with importing resources from ENEX files 2020-06-14 16:26:35 +00:00
Laurent Cozic
95d8ee65a5 ios-v10.0.49 2020-06-14 17:04:10 +01:00
30 changed files with 218 additions and 106 deletions

View File

@@ -61,6 +61,7 @@ Modules/TinyMCE/IconPack/postinstall.js
Modules/TinyMCE/langs/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
ElectronClient/global.d.js
ElectronClient/gui/MultiNoteActions.js
ElectronClient/gui/NoteContentPropertiesDialog.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js

1
.gitignore vendored
View File

@@ -51,6 +51,7 @@ Tools/commit_hook.txt
*.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
ElectronClient/global.d.js
ElectronClient/gui/MultiNoteActions.js
ElectronClient/gui/NoteContentPropertiesDialog.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js

View File

@@ -21,7 +21,11 @@ class Command extends BaseCommand {
.filter(m => m.type === 'importer')
.map(m => m.format);
return [['--format <format>', _('Source format: %s', ['auto'].concat(formats).join(', '))], ['-f, --force', _('Do not ask for confirmation.')]];
return [
['--format <format>', _('Source format: %s', ['auto'].concat(formats).join(', '))],
['-f, --force', _('Do not ask for confirmation.')],
['--output-format <output-format>', _('Output format: %s', 'md, html')],
];
}
async action(args) {
@@ -55,9 +59,9 @@ class Command extends BaseCommand {
this.stdout(s);
};
app()
.gui()
.showConsole();
if (args.options.outputFormat) importOptions.outputFormat = args.options.outputFormat;
app().gui().showConsole();
this.stdout(_('Importing notes...'));
const service = new InteropService();
const result = await service.import(importOptions);

View File

@@ -52,8 +52,6 @@ BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', `net.cozic.joplin${env === 'dev' ? 'dev' : ''}-cli`);
Setting.setConstant('appType', 'cli');
console.info(Setting.value('appId'));
shimInit();
const application = app();

View File

@@ -89,20 +89,20 @@ describe('EnexToHtml', function() {
}],
});
it('fails when not given a matching resource', asyncTest(async () => {
// To test the promise-unexpectedly-resolved case, add `audioResource` to the array.
const resources = [];
const inputFile = fileWithPath('en-media--image.enex');
const enexInput = await shim.fsDriver().readFile(inputFile);
const promisedOutput = enexXmlToHtml(enexInput, resources);
// it('fails when not given a matching resource', asyncTest(async () => {
// // To test the promise-unexpectedly-resolved case, add `audioResource` to the array.
// const resources = [];
// const inputFile = fileWithPath('en-media--image.enex');
// const enexInput = await shim.fsDriver().readFile(inputFile);
// const promisedOutput = enexXmlToHtml(enexInput, resources);
promisedOutput.then(() => {
// Promise should not be resolved
expect(false).toEqual(true);
}, (reason) => {
expect(reason)
.toBe('Hash with no associated resource: 89ce7da62c6b2832929a6964237e98e9');
});
}));
// promisedOutput.then(() => {
// // Promise should not be resolved
// expect(false).toEqual(true);
// }, (reason) => {
// expect(reason)
// .toBe('Hash with no associated resource: 89ce7da62c6b2832929a6964237e98e9');
// });
// }));
});

View File

@@ -2,6 +2,5 @@
<div><a href="joplin://21ca2b948f222a38802940ec7e2e5de3" hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" alt="attachment-1">attachment-1</a></div>
<div>
<br>
<br>
</div>
</en-note>

View File

@@ -8,6 +8,5 @@
</div>
<div>
<br>
<br>
</div>
</en-note>

View File

@@ -4,11 +4,9 @@
<div>
<input type="checkbox" onclick="return false;">A test for <i>italic</i>
<br>
<br>
</div>
<div>
<br>
<br>
</div>
<div><i><img src=":/89ce7da62c6b2832929a6964237e98e9" hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" alt=""></i></div>
</en-note>

View File

@@ -226,8 +226,9 @@
const nodeName = node.nodeName.toLowerCase();
const nodeParent = node.parentNode;
const nodeParentName = nodeParent ? nodeParent.nodeName.toLowerCase() : '';
const computedStyle = node.nodeType === 1 ? window.getComputedStyle(node) : {};
let isVisible = node.nodeType === 1 ? window.getComputedStyle(node).display !== 'none' : true;
let isVisible = node.nodeType === 1 ? computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden' : true;
if (isVisible && ['script', 'noscript', 'style', 'select', 'option', 'button'].indexOf(nodeName) >= 0) isVisible = false;
// If it's a text input or a textarea and it has a value, save

View File

@@ -4,6 +4,7 @@ const InteropService = require('lib/services/InteropService');
const Setting = require('lib/models/Setting');
const Note = require('lib/models/Note.js');
const { friendlySafeFilename } = require('lib/path-utils');
const { time } = require('lib/time-utils.js');
const md5 = require('md5');
const url = require('url');
const { shim } = require('lib/shim');
@@ -108,10 +109,16 @@ class InteropServiceHelper {
}
static async defaultFilename(noteId, fileExtension) {
if (!noteId) return '';
const note = await Note.load(noteId);
// In a rare case the passed not will be null, use the id for filename
const filename = friendlySafeFilename(note ? note.title : noteId, 100);
// Default filename is just the date
const date = time.formatMsToLocal(new Date().getTime(), time.dateFormat());
let filename = friendlySafeFilename(`${date}`, 100);
if (noteId) {
const note = await Note.load(noteId);
// In a rare case the passed note will be null, use the id for filename
filename = friendlySafeFilename(note ? note.title : noteId, 100);
}
return `${filename}.${fileExtension}`;
}

4
ElectronClient/global.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
// Declare codemirror module so that we can import it using the import syntax.
// This also means it will implicitly have the any type, which is necessary because
// of the flexible manner that codemirror is made https://discuss.codemirror.net/t/basic-codemirror-configuration-in-typescript-project/2047/2
declare module 'codemirror';

View File

@@ -465,12 +465,6 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
};
}, [editor, onEditorPaste, onEditorContextMenu]);
useEffect(() => {
// We disable dragging ot text because it's not really supported, and
// can lead to data loss: https://github.com/laurent22/joplin/issues/3302
if (editor) editor.setOption('dragEnabled', false);
}, [editor]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
}, []);

View File

@@ -1,7 +1,8 @@
import * as React from 'react';
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
const CodeMirror = require('codemirror');
import * as CodeMirror from 'codemirror';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/edit/closebrackets';
@@ -15,6 +16,7 @@ import useLineSorting from './utils/useLineSorting';
import 'codemirror/keymap/emacs';
import 'codemirror/keymap/vim';
import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/xml/xml';
@@ -134,6 +136,12 @@ function Editor(props: EditorProps, ref: any) {
extraKeys: { 'Enter': 'insertListElement',
'Ctrl-/': 'toggleComment',
'Ctrl-Alt-S': 'sortSelectedLines',
'Alt-Up': 'swapLineUp',
'Alt-Down': 'swapLineDown',
'Cmd-/': 'toggleComment',
'Cmd-Opt-S': 'sortSelectedLines',
'Opt-Up': 'swapLineUp',
'Opt-Down': 'swapLineDown',
'Tab': 'smartListIndent',
'Shift-Tab': 'smartListUnindent' },
};

View File

@@ -47,16 +47,16 @@ export default function useListIdent(CodeMirror: any) {
return currentToken;
}
// Gets the first non-whitespace token locationof a list
function getListSpan(listTokens: any) {
// Gets the character coordinates of the start and end of a list token
function getListSpan(listTokens: any, line: string) {
let start = listTokens[0].start;
const end = listTokens[listTokens.length - 1].end;
const token = extractListToken(line);
if (listTokens.length > 1 && listTokens[0].string.match(/\s/)) {
start = listTokens[1].start;
}
return { start: start, end: end };
return { start: start, end: start + token.length };
}
CodeMirror.commands.smartListIndent = function(cm: any) {
@@ -74,7 +74,7 @@ export default function useListIdent(CodeMirror: any) {
} else {
if (olLineNumber(line)) {
const tokens = cm.getLineTokens(anchor.line);
const { start, end } = getListSpan(tokens);
const { start, end } = getListSpan(tokens, line);
// Resets numbered list to 1.
cm.replaceRange('1. ', { line: anchor.line, ch: start }, { line: anchor.line, ch: end });
}
@@ -99,9 +99,9 @@ export default function useListIdent(CodeMirror: any) {
} else {
const newToken = newListToken(cm, anchor.line);
const tokens = cm.getLineTokens(anchor.line);
const { start, end } = getListSpan(tokens);
const { start, end } = getListSpan(tokens, line);
cm.replaceRange(newToken, { line: anchor.line, ch: start }, { line: anchor.line, ch: end });
cm.replaceRange(newToken, { line: anchor.line, ch: start }, { line: anchor.line, ch: end });
cm.indentLine(anchor.line, 'subtract');
}
@@ -126,7 +126,15 @@ export default function useListIdent(CodeMirror: any) {
cm.replaceRange('', { line: anchor.line, ch: 0 }, anchor);
}
} else {
cm.execCommand('newlineAndIndentContinueMarkdownList');
// Disable automatic indent for html/xml outside of codeblocks
const state = cm.getTokenAt(anchor).state;
const mode = cm.getModeAt(anchor);
// html/xml inside of a codeblock is fair game for auto-indent
if (mode.name !== 'xml' || state.overlay.codeBlock) {
cm.execCommand('newlineAndIndentContinueMarkdownList');
} else {
cm.replaceSelection('\n');
}
}
}
};

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.220",
"version": "1.0.221",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.220",
"version": "1.0.221",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {

View File

@@ -10,9 +10,16 @@ async function main() {
for (const destDir of destDirs) {
console.info(`Copying to ${destDir}`);
await fs.remove(destDir);
await fs.mkdirp(destDir);
await fs.copy(sourceDir, destDir);
try {
await fs.remove(destDir);
await fs.copy(sourceDir, destDir);
} catch (error) {
// These calls randomly fail on Windows when the folders are being
// watch by TypeScript. As these files aren't always needed for
// development, only print a warning.
console.warn(`Could not copy to ${destDir}`, error);
}
}
}

View File

@@ -22,7 +22,7 @@ Operating System | Download | Alternative
-----------------|--------|-------------------
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.216/Joplin-Setup-1.0.216.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.216/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.216/Joplin-1.0.216.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | You can also use Homebrew (unsupported): `brew cask install joplin`
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.216/Joplin-1.0.216.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package (unsupported) [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, Gnome and Mint), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh \| bash`
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.216/Joplin-1.0.216.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package (unsupported) [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, and Mint; the desktop environments supported are GNOME, KDE, Xfcxe, MATE, LXQT, LXDE, Unity, Cinnamon, Deepin and Pantheon), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh \| bash`
## Mobile applications
@@ -61,9 +61,9 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
* * *
| | | |
| :---: | :---: | :---: | :---: |
| :---: | :---: | :---: |
| <img width="50" src="https://avatars0.githubusercontent.com/u/6979755?s=96&v=4"/></br>[Devon Zuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[小西 孝宗](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[Alexander van der Berg](https://github.com/avanderberg)
| <img width="50" src="https://avatars0.githubusercontent.com/u/1168659?s=96&v=4"/></br>[Nicholas Head](https://github.com/nicholashead)
| <img width="50" src="https://avatars0.githubusercontent.com/u/1168659?s=96&v=4"/></br>[Nicholas Head](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[Frank Bloise](https://github.com/fbloise)
<!-- TOC -->
# Table of contents

View File

@@ -170,7 +170,7 @@
isa = PBXProject;
attributes = {
LastUpgradeCheck = 610;
ORGANIZATIONNAME = Facebook;
ORGANIZATIONNAME = joplinapp.org;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = A9BXAFS6CT;
@@ -337,7 +337,7 @@
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 49;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = A9BXAFS6CT;
HEADER_SEARCH_PATHS = (
@@ -357,7 +357,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = "$(inherited)";
MARKETING_VERSION = 10.0.48;
MARKETING_VERSION = 10.0.49;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -380,7 +380,7 @@
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_TEAM = A9BXAFS6CT;
HEADER_SEARCH_PATHS = (
"$(inherited)",
@@ -393,7 +393,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = "$(inherited)";
MARKETING_VERSION = 10.0.48;
MARKETING_VERSION = 10.0.49;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

@@ -7,6 +7,28 @@ const htmlentities = new Entities().encode;
const imageRegex = /<img([\s\S]*?)src=["']([\s\S]*?)["']([\s\S]*?)>/gi;
const anchorRegex = /<a([\s\S]*?)href=["']([\s\S]*?)["']([\s\S]*?)>/gi;
const selfClosingElements = [
'area',
'base',
'basefont',
'br',
'col',
'command',
'embed',
'frame',
'hr',
'img',
'input',
'isindex',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
];
class HtmlUtils {
headAndBodyHtml(doc) {
const output = [];
@@ -15,6 +37,10 @@ class HtmlUtils {
return output.join('\n');
}
isSelfClosingTag(tagName) {
return selfClosingElements.includes(tagName.toLowerCase());
}
extractImageUrls(html) {
if (!html) return [];

View File

@@ -1,6 +1,9 @@
const stringToStream = require('string-to-stream');
const cleanHtml = require('clean-html');
const resourceUtils = require('lib/resourceUtils.js');
const { isSelfClosingTag } = require('lib/htmlUtils');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
function addResourceTag(lines, resource, attributes) {
// Note: refactor to use Resource.markdownTag
@@ -56,7 +59,7 @@ function enexXmlToHtml_(stream, resources) {
}
};
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
const options = {};
const strict = false;
const saxStream = require('sax').createStream(strict, options);
@@ -69,12 +72,11 @@ function enexXmlToHtml_(stream, resources) {
saxStream.on('error', function(e) {
console.warn(e);
// reject(e);
});
saxStream.on('text', function(text) {
section.lines.push(text);
section.lines.push(htmlentities(text));
});
saxStream.on('opentag', function(node) {
@@ -110,7 +112,7 @@ function enexXmlToHtml_(stream, resources) {
}
if (!found) {
reject(`Hash with no associated resource: ${hash}`);
// console.warn(`Hash with no associated resource: ${hash}`);
}
}
@@ -122,16 +124,16 @@ function enexXmlToHtml_(stream, resources) {
}
} else if (tagName == 'en-todo') {
section.lines.push('<input type="checkbox" onclick="return false;" />');
} else if (node.isSelfClosing) {
section.lines.push(`<${tagName}${attributesStr}>`);
} else if (isSelfClosingTag(tagName)) {
section.lines.push(`<${tagName}${attributesStr}/>`);
} else {
section.lines.push(`<${tagName}${attributesStr} />`);
section.lines.push(`<${tagName}${attributesStr}>`);
}
});
saxStream.on('closetag', function(n) {
const tagName = n ? n.toLowerCase() : n;
section.lines.push(`</${tagName}>`);
saxStream.on('closetag', function(node) {
const tagName = node ? node.toLowerCase() : node;
if (!isSelfClosingTag(tagName)) section.lines.push(`</${tagName}>`);
});
saxStream.on('attribute', function() {});
@@ -151,19 +153,19 @@ async function enexXmlToHtml(xmlString, resources, options = {}) {
const stream = stringToStream(xmlString);
const result = await enexXmlToHtml_(stream, resources, options);
try {
const preCleaning = result.content.lines.join(''); // xmlString
const final = await beautifyHtml(preCleaning);
return final.join('');
} catch (error) {
console.warn(error);
}
const preCleaning = result.content.lines.join('');
const final = await beautifyHtml(preCleaning);
return final.join('');
}
const beautifyHtml = (html) => {
return new Promise((resolve) => {
const options = { wrap: 0 };
cleanHtml.clean(html, options, (...cleanedHtml) => resolve(cleanedHtml));
try {
cleanHtml.clean(html, { wrap: 0 }, (...cleanedHtml) => resolve(cleanedHtml));
} catch (error) {
console.warn(`Could not clean HTML - the "unclean" version will be used: ${error.message}: ${html.trim().substr(0, 512).replace(/[\n\r]/g, ' ')}...`);
resolve([html]);
}
});
};

View File

@@ -688,7 +688,7 @@ function enexXmlToMdArray(stream, resources) {
}
if (!found) {
console.warn(`Hash with no associated resource: ${hash}`);
// console.warn(`Hash with no associated resource: ${hash}`);
}
}

View File

@@ -38,13 +38,28 @@ function extractRecognitionObjId(recognitionXml) {
return r && r.length >= 2 ? r[1] : null;
}
async function decodeBase64File(sourceFile, destFile) {
async function decodeBase64File(sourceFilePath, destFilePath) {
return new Promise(function(resolve, reject) {
const sourceStream = fs.createReadStream(sourceFile);
const destStream = fs.createWriteStream(destFile);
// Note: we manually handle closing the file so that we can
// force flusing it before close. This is needed because
// "end" might be called before the file has been flushed
// to disk, thus resulting in the calling code to find a
// file with size 0.
const destFile = fs.openSync(destFilePath, 'w');
const sourceStream = fs.createReadStream(sourceFilePath);
const destStream = fs.createWriteStream(destFile, {
fd: destFile,
autoClose: false,
});
sourceStream.pipe(new Base64Decode()).pipe(destStream);
sourceStream.on('end', () => resolve());
sourceStream.on('end', () => {
fs.fdatasyncSync(destFile);
fs.closeSync(destFile);
resolve();
});
sourceStream.on('error', (error) => reject(error));
});
}

View File

@@ -5,7 +5,7 @@ const { setupLinkify } = require('lib/joplin-renderer');
const removeMarkdown = require('remove-markdown');
// Taken from codemirror/addon/edit/continuelist.js
const listRegex = /^(\s*)([*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
const listRegex = /^(\s*)([*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]\s))(\s*)/;
const emptyListRegex = /^(\s*)([*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
const markdownUtils = {

View File

@@ -223,6 +223,8 @@ class Folder extends BaseItem {
static async expandTree(folders, parentId) {
const folderPath = await this.folderPath(folders, parentId);
folderPath.pop(); // We don't expand the leaft notebook
for (const folder of folderPath) {
this.dispatch({
type: 'FOLDER_SET_COLLAPSED',

View File

@@ -140,7 +140,7 @@ class Note extends BaseItem {
useAbsolutePaths: false,
}, options);
this.logger().info('replaceResourceInternalToExternalLinks', 'options:', options, 'body:', body);
this.logger().debug('replaceResourceInternalToExternalLinks', 'options:', options, 'body:', body);
const resourceIds = await this.linkedResourceIds(body);
const Resource = this.getClass('Resource');
@@ -153,7 +153,7 @@ class Note extends BaseItem {
body = body.replace(new RegExp(`:/${id}`, 'gi'), markdownUtils.escapeLinkUrl(resourcePath));
}
this.logger().info('replaceResourceInternalToExternalLinks result', body);
this.logger().debug('replaceResourceInternalToExternalLinks result', body);
return body;
}
@@ -171,7 +171,7 @@ class Note extends BaseItem {
pathsToTry.push(Resource.baseRelativeDirectoryPath());
}
this.logger().info('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry, 'body:', body);
this.logger().debug('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry, 'body:', body);
for (const basePath of pathsToTry) {
const reStrings = [
@@ -189,7 +189,7 @@ class Note extends BaseItem {
}
}
this.logger().info('replaceResourceExternalToInternalLinks result', body);
this.logger().debug('replaceResourceExternalToInternalLinks result', body);
return body;
}

View File

@@ -26,6 +26,21 @@ class ExternalEditWatcher {
return this.instance_;
}
externalApi() {
return {
openAndWatch: async ({ noteId }) => {
const note = await Note.load(noteId);
return this.openAndWatch(note);
},
stopWatching: async ({ noteId }) => {
return this.stopWatching(noteId);
},
noteIsWatched: async ({ noteId }) => {
return this.noteIsWatched(noteId);
},
};
}
tempDir() {
return Setting.value('profileDir');
}

View File

@@ -55,6 +55,7 @@ class InteropService {
description: _('Evernote Export File (as HTML)'),
// TODO: Consider doing this the same way as the multiple `md` importers are handled
importerClass: 'InteropService_Importer_EnexToHtml',
outputFormat: 'html',
},
];
@@ -97,14 +98,11 @@ class InteropService {
importModules = importModules.map(a => {
const className = a.importerClass || `InteropService_Importer_${toTitleCase(a.format)}`;
const output = Object.assign(
{},
{
type: 'importer',
path: `lib/services/${className}`,
},
a
);
const output = Object.assign({}, {
type: 'importer',
path: `lib/services/${className}`,
outputFormat: 'md',
}, a);
if (!('isNoteArchive' in output)) output.isNoteArchive = true;
return output;
});
@@ -142,15 +140,17 @@ class InteropService {
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
// is returned. This is useful to auto-detect the module based on the format.
// For more precise matching, newModuleFromPath_ should be used.
findModuleByFormat_(type, format, target = null) {
findModuleByFormat_(type, format, target = null, outputFormat = null) {
const modules = this.modules();
const matches = [];
for (let i = 0; i < modules.length; i++) {
const m = modules[i];
if (m.format === format && m.type === type) {
if (target === null) {
if (!target && !outputFormat) {
matches.push(m);
} else if (target === m.target) {
} else if (target && target === m.target) {
matches.push(m);
} else if (outputFormat && outputFormat === m.outputFormat) {
matches.push(m);
}
}
@@ -169,9 +169,9 @@ class InteropService {
* https://github.com/laurent22/joplin/pull/1795#discussion_r322379121) but
* we can do it if it ever becomes necessary.
*/
newModuleByFormat_(type, format) {
const moduleMetadata = this.findModuleByFormat_(type, format);
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s"', type, format));
newModuleByFormat_(type, format, outputFormat = 'md') {
const moduleMetadata = this.findModuleByFormat_(type, format, null, outputFormat);
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and output "%s"', type, format, outputFormat));
const ModuleClass = require(moduleMetadata.path);
const output = new ModuleClass();
output.setMetadata(moduleMetadata);
@@ -243,15 +243,12 @@ class InteropService {
let result = { warnings: [] };
// console.log('options passed to InteropService:');
// console.log(JSON.stringify({options}, null, 2));
let importer = null;
if (options.modulePath) {
importer = this.newModuleFromPath_('importer', options);
} else {
importer = this.newModuleByFormat_('importer', options.format);
importer = this.newModuleByFormat_('importer', options.format, options.outputFormat);
}
await importer.init(options.path, options);

View File

@@ -25,6 +25,8 @@ const uri2path = require('file-uri-to-path');
const { MarkupToHtml } = require('lib/joplin-renderer');
const { uuid } = require('lib/uuid');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
class ApiError extends Error {
constructor(message, httpCode = 400) {
super(message);
@@ -377,6 +379,27 @@ class Api {
return options;
}
async execServiceActionFromRequest_(externalApi, request) {
const action = externalApi[request.action];
if (!action) throw new ErrorNotFound(`Invalid action: ${request.action}`);
const args = Object.assign({}, request);
delete args.action;
return action(args);
}
async action_services(request, serviceName) {
this.checkToken_(request);
if (request.method !== 'POST') throw new ErrorMethodNotAllowed();
if (serviceName === 'externalEditWatcher') {
const externalApi = ExternalEditWatcher.instance().externalApi();
return this.execServiceActionFromRequest_(externalApi, JSON.parse(request.body));
} else {
throw new ErrorNotFound(`No such service: ${serviceName}`);
}
}
async action_notes(request, id = null, link = null) {
this.checkToken_(request);

View File

@@ -12,7 +12,7 @@ const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
const { shim } = require('lib/shim.js');
const { filename } = require('lib/path-utils');
const { filename, fileExtension } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
const BaseSyncTarget = require('lib/BaseSyncTarget');
const TaskQueue = require('lib/TaskQueue');
@@ -222,7 +222,10 @@ class Synchronizer {
async lockFiles_() {
const output = await this.api().list(this.lockDirName_);
return output.items;
return output.items.filter((p) => {
const ext = fileExtension(p.path);
return ext === 'lock';
});
}
parseLockFilePath(path) {