You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-05 20:56:22 +02:00
Compare commits
23 Commits
android-v1
...
v1.0.221
Author | SHA1 | Date | |
---|---|---|---|
|
acc10ccac4 | ||
|
45160a2e73 | ||
|
e38794171a | ||
|
c2d6da83c0 | ||
|
77005fc495 | ||
|
0273e58783 | ||
|
3e88a24753 | ||
|
1205908233 | ||
|
3a7a068196 | ||
|
845ecfe742 | ||
|
53355aaad3 | ||
|
7cd0e25538 | ||
|
2f15178ff6 | ||
|
a4b13be0d1 | ||
|
23ae4fb790 | ||
|
cff036d08b | ||
|
7826cc0b03 | ||
|
2bfa28a311 | ||
|
780c5c80ae | ||
|
fcd00b3212 | ||
|
6a41d6e85a | ||
|
3733858145 | ||
|
95d8ee65a5 |
@@ -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
1
.gitignore
vendored
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
@@ -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');
|
||||
// });
|
||||
// }));
|
||||
|
||||
});
|
||||
|
@@ -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>
|
@@ -8,6 +8,5 @@
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
</en-note>
|
@@ -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>
|
@@ -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
|
||||
|
@@ -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
4
ElectronClient/global.d.ts
vendored
Normal 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';
|
@@ -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);
|
||||
}, []);
|
||||
|
@@ -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' },
|
||||
};
|
||||
|
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
2
ElectronClient/package-lock.json
generated
2
ElectronClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.221",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.221",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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 [];
|
||||
|
||||
|
@@ -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]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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));
|
||||
});
|
||||
}
|
||||
|
@@ -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 = {
|
||||
|
@@ -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',
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user