mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-27 10:32:58 +02:00
Merge branch 'master' of github.com:laurent22/joplin
This commit is contained in:
commit
145ee13356
@ -177,22 +177,33 @@ class Application extends BaseApplication {
|
||||
await doExit();
|
||||
}
|
||||
|
||||
commands() {
|
||||
if (this.allCommandsLoaded_) return this.commands_;
|
||||
commands(uiType = null) {
|
||||
if (!this.allCommandsLoaded_) {
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
this.allCommandsLoaded_ = true;
|
||||
}
|
||||
|
||||
this.allCommandsLoaded_ = true;
|
||||
if (uiType !== null) {
|
||||
let temp = [];
|
||||
for (let n in this.commands_) {
|
||||
if (!this.commands_.hasOwnProperty(n)) continue;
|
||||
const c = this.commands_[n];
|
||||
if (!c.supportsUi(uiType)) continue;
|
||||
temp[n] = c;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
return this.commands_;
|
||||
}
|
||||
@ -310,6 +321,8 @@ class Application extends BaseApplication {
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
|
@ -12,6 +12,10 @@ class Command extends BaseCommand {
|
||||
return _('Exits the application.');
|
||||
}
|
||||
|
||||
compatibleUis() {
|
||||
return ['gui'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await app().exit();
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
allCommands() {
|
||||
const commands = app().commands();
|
||||
const commands = app().commands(app().uiType());
|
||||
let output = [];
|
||||
for (let n in commands) {
|
||||
if (!commands.hasOwnProperty(n)) continue;
|
||||
@ -69,6 +69,8 @@ class Command extends BaseCommand {
|
||||
this.stdout('');
|
||||
this.stdout(_('The possible commands are:'));
|
||||
this.stdout('');
|
||||
this.stdout(_('Type `help all` for the complete help of all the commands.'));
|
||||
this.stdout('');
|
||||
this.stdout(commandNames.join(', '));
|
||||
this.stdout('');
|
||||
this.stdout(_('In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));
|
||||
|
@ -2,6 +2,7 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
@ -12,16 +13,16 @@ class Command extends BaseCommand {
|
||||
return 'set <note> <name> [value]';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Sets the property <name> of the given <note> to the given [value].');
|
||||
}
|
||||
const fields = Note.fields();
|
||||
const s = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const f = fields[i];
|
||||
if (f.name === 'id') continue;
|
||||
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
|
||||
}
|
||||
|
||||
hidden() {
|
||||
return true;
|
||||
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
|
@ -32,7 +32,6 @@ class Command extends BaseCommand {
|
||||
options() {
|
||||
return [
|
||||
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
|
||||
['--random-failures', 'For debugging purposes. Do not use.'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -140,7 +139,6 @@ class Command extends BaseCommand {
|
||||
cliUtils.redrawDone();
|
||||
this.stdout(msg);
|
||||
},
|
||||
randomFailures: args.options['random-failures'] === true,
|
||||
};
|
||||
|
||||
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
|
||||
|
@ -18,8 +18,8 @@ class Command extends BaseCommand {
|
||||
return { data: autocompleteFolders };
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
compatibleUis() {
|
||||
return ['cli'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
|
2
CliClient/package-lock.json
generated
2
CliClient/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.78",
|
||||
"version": "0.10.79",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -18,7 +18,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.78",
|
||||
"version": "0.10.79",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
|
@ -39,10 +39,22 @@ class Bridge {
|
||||
return this.window().setSize(width, height);
|
||||
}
|
||||
|
||||
showSaveDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
const filePath = dialog.showSaveDialog(options);
|
||||
if (filePath) {
|
||||
this.lastSelectedPath_ = filePath;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
showOpenDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const filePaths = dialog.showOpenDialog(options);
|
||||
if (filePaths && filePaths.length) {
|
||||
this.lastSelectedPath_ = dirname(filePaths[0]);
|
||||
@ -71,6 +83,15 @@ class Bridge {
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
showInfoMessageBox(message) {
|
||||
const result = this.showMessageBox({
|
||||
type: 'info',
|
||||
message: message,
|
||||
buttons: [_('OK')],
|
||||
});
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
get Menu() {
|
||||
return require('electron').Menu;
|
||||
}
|
||||
|
@ -41,19 +41,24 @@ class ImportScreenComponent extends React.Component {
|
||||
const messages = this.state.messages.slice();
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (messages[i].key === key) {
|
||||
messages[i].text = text;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) messages.push({ key: key, text: text });
|
||||
messages.push({ key: key, text: text });
|
||||
|
||||
this.setState({ messages: messages });
|
||||
}
|
||||
|
||||
uniqueMessages() {
|
||||
let output = [];
|
||||
const messages = this.state.messages.slice();
|
||||
let foundKeys = [];
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (foundKeys.indexOf(msg.key) >= 0) continue;
|
||||
foundKeys.push(msg.key);
|
||||
output.unshift(msg);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async doImport() {
|
||||
const filePath = this.props.filePath;
|
||||
const folderTitle = await Folder.findUniqueFolderTitle(filename(filePath));
|
||||
@ -77,10 +82,9 @@ class ImportScreenComponent extends React.Component {
|
||||
this.addMessage('progress', lastProgress);
|
||||
},
|
||||
onError: (error) => {
|
||||
const messages = this.state.messages.slice();
|
||||
let s = error.trace ? error.trace : error.toString();
|
||||
messages.push({ key: 'error_' + (progressCount++), text: s });
|
||||
this.addMessage('error_' + (progressCount++), lastProgress);
|
||||
// Don't display the error directly because most of the time it doesn't matter
|
||||
// (eg. for weird broken HTML, but the note is still imported)
|
||||
console.warn('When importing ENEX file', error);
|
||||
},
|
||||
}
|
||||
|
||||
@ -95,7 +99,7 @@ class ImportScreenComponent extends React.Component {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const messages = this.state.messages;
|
||||
const messages = this.uniqueMessages();
|
||||
|
||||
const messagesStyle = {
|
||||
padding: 10,
|
||||
|
@ -288,8 +288,9 @@ class MainScreenComponent extends React.Component {
|
||||
const promptOptions = this.state.promptOptions;
|
||||
const folders = this.props.folders;
|
||||
const notes = this.props.notes;
|
||||
const messageBoxVisible = this.props.hasDisabledSyncItems;
|
||||
|
||||
const styles = this.styles(this.props.theme, style.width, style.height, true);
|
||||
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerButtons = [];
|
||||
@ -342,7 +343,7 @@ class MainScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
const messageComp = this.props.hasDisabledSyncItems ? (
|
||||
const messageComp = messageBoxVisible ? (
|
||||
<div style={styles.messageBox}>
|
||||
<span style={theme.textStyle}>
|
||||
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
|
||||
|
@ -174,7 +174,7 @@ class NoteListComponent extends React.Component {
|
||||
}, style);
|
||||
emptyDivStyle.width = emptyDivStyle.width - padding * 2;
|
||||
emptyDivStyle.height = emptyDivStyle.height - padding * 2;
|
||||
return <div style={emptyDivStyle}>{_('No notes in here. Create one by clicking on "New note".')}</div>
|
||||
return <div style={emptyDivStyle}>{ this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
@ -193,6 +193,7 @@ class NoteListComponent extends React.Component {
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
theme: state.settings.theme,
|
||||
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
|
@ -7,6 +7,7 @@ const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class StatusScreenComponent extends React.Component {
|
||||
|
||||
@ -27,6 +28,21 @@ class StatusScreenComponent extends React.Component {
|
||||
this.setState({ report: report });
|
||||
}
|
||||
|
||||
async exportDebugReportClick() {
|
||||
const filename = 'syncReport-' + (new Date()).getTime() + '.csv';
|
||||
|
||||
const filePath = bridge().showSaveDialog({
|
||||
title: _('Please select where the sync status should be exported to'),
|
||||
defaultPath: filename,
|
||||
});
|
||||
|
||||
if (!filePath) return;
|
||||
|
||||
const service = new ReportService();
|
||||
const csv = await service.basicItemList({ format: 'csv' });
|
||||
await fs.writeFileSync(filePath, csv);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
@ -97,6 +113,7 @@ class StatusScreenComponent extends React.Component {
|
||||
<div style={style}>
|
||||
<Header style={headerStyle} />
|
||||
<div style={containerStyle}>
|
||||
<a style={theme.textStyle} onClick={() => this.exportDebugReportClick()}href="#">Export debug report</a>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
|
2
ElectronClient/app/package-lock.json
generated
2
ElectronClient/app/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.37",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.37",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@ -25,6 +25,10 @@
|
||||
"win": {
|
||||
"icon": "../../Assets/Joplin.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"mac": {
|
||||
"icon": "../../Assets/macOs.icns",
|
||||
"asar": false
|
||||
|
@ -18,9 +18,9 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
|
||||
|
||||
Operating System | Download
|
||||
-----------------|--------
|
||||
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.36/Joplin-Setup-0.10.36.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.36/Joplin-0.10.36.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.36/Joplin-0.10.36-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
|
||||
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-Setup-0.10.37.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-0.10.37.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-0.10.37-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
|
||||
|
||||
## Mobile applications
|
||||
|
||||
|
@ -137,6 +137,25 @@ Since this is still an actual URL, the terminal will still make it clickable. An
|
||||
|
||||
In Markdown, links to resources are represented as a simple ID to the resource. In order to give access to these resources, they will be, like links, converted to local URLs. Clicking this link will then open a browser, which will handle the file - i.e. display the image, open the PDF file, etc.
|
||||
|
||||
# Shell mode
|
||||
|
||||
Commands can also be used directly from a shell. To view the list of available commands, type `joplin help all`. To reference a note, notebook or tag you can either use the ID (type `joplin ls -l` to view the ID) or by title.
|
||||
|
||||
For example, this will create a new note "My note" in the notebook "My notebook":
|
||||
|
||||
$ joplin mkbook "My notebook"
|
||||
$ joplin use "My notebook"
|
||||
$ joplin mknote "My note"
|
||||
|
||||
To view the newly created note:
|
||||
|
||||
$ joplin ls -l
|
||||
fe889 07/12/2017 17:57 My note
|
||||
|
||||
Give a new title to the note:
|
||||
|
||||
$ joplin set fe889 title "New title"
|
||||
|
||||
# Available shortcuts
|
||||
|
||||
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of shortcuts:
|
||||
|
@ -66,10 +66,15 @@ class BaseApplication {
|
||||
}
|
||||
|
||||
switchCurrentFolder(folder) {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : '',
|
||||
});
|
||||
if (!this.hasGui()) {
|
||||
this.currentFolder_ = Object.assign({}, folder);
|
||||
Setting.setValue('activeFolderId', folder ? folder.id : '');
|
||||
} else {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the initial flags passed to main script and
|
||||
@ -227,6 +232,10 @@ class BaseApplication {
|
||||
return false;
|
||||
}
|
||||
|
||||
uiType() {
|
||||
return this.hasGui() ? 'gui' : 'cli';
|
||||
}
|
||||
|
||||
generalMiddlewareFn() {
|
||||
const middleware = store => next => (action) => {
|
||||
return this.generalMiddleware(store, next, action);
|
||||
|
@ -100,7 +100,7 @@ class MdToHtml {
|
||||
const href = this.getAttr_(attrs, 'src');
|
||||
|
||||
if (!Resource.isResourceUrl(href)) {
|
||||
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
||||
return '<img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
||||
}
|
||||
|
||||
const resourceId = Resource.urlToId(href);
|
||||
|
@ -1,6 +1,6 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image } = require('react-native');
|
||||
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { Log } = require('lib/log.js');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
@ -218,7 +218,7 @@ class ScreenHeaderComponent extends Component {
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = RNFS.ExternalDirectoryPath + '/syncReport-' + (new Date()).getTime() + '.txt';
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join("\n--------------------------------------------------------------------------------");
|
||||
const finalText = [logItemCsv, itemListCsv].join("\n================================================================================\n");
|
||||
|
||||
await RNFS.writeFile(filePath, finalText);
|
||||
alert('Debug report exported to ' + filePath);
|
||||
@ -410,6 +410,7 @@ class ScreenHeaderComponent extends Component {
|
||||
const backButtonComp = backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack);
|
||||
const searchButtonComp = this.props.noteSelectionEnabled ? null : searchButton(this.styles(), () => this.searchButton_press());
|
||||
const deleteButtonComp = this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press()) : null;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
|
||||
const menuComp = (
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
@ -417,7 +418,9 @@ class ScreenHeaderComponent extends Component {
|
||||
<Text style={this.styles().contextMenuTrigger}> ⋮</Text>
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
{ menuOptionComponents }
|
||||
<ScrollView style={{ maxHeight: windowHeight }}>
|
||||
{ menuOptionComponents }
|
||||
</ScrollView>
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
);
|
||||
|
@ -29,9 +29,11 @@ shared.saveNoteButton_press = async function(comp) {
|
||||
}
|
||||
|
||||
let isNew = !note.id;
|
||||
let titleWasAutoAssigned = false;
|
||||
|
||||
if (isNew && !note.title) {
|
||||
note.title = Note.defaultTitle(note);
|
||||
titleWasAutoAssigned = true;
|
||||
}
|
||||
|
||||
// Save only the properties that have changed
|
||||
@ -54,8 +56,11 @@ shared.saveNoteButton_press = async function(comp) {
|
||||
// But we preserve the current title and body because
|
||||
// the user might have changed them between the time
|
||||
// saveNoteButton_press was called and the note was
|
||||
// saved (it's done asynchronously)
|
||||
note.title = stateNote.title;
|
||||
// saved (it's done asynchronously).
|
||||
//
|
||||
// If the title was auto-assigned above, we don't restore
|
||||
// it from the state because it will be empty there.
|
||||
if (!titleWasAutoAssigned) note.title = stateNote.title;
|
||||
note.body = stateNote.body;
|
||||
}
|
||||
|
||||
|
@ -165,6 +165,16 @@ class Database {
|
||||
throw new Error('Unknown enum type or value: ' + type + ', ' + s);
|
||||
}
|
||||
|
||||
static enumName(type, id) {
|
||||
if (type === 'fieldType') {
|
||||
if (id === Database.TYPE_UNKNOWN) return 'unknown';
|
||||
if (id === Database.TYPE_INT) return 'int';
|
||||
if (id === Database.TYPE_TEXT) return 'text';
|
||||
if (id === Database.TYPE_NUMERIC) return 'numeric';
|
||||
throw new Error('Invalid type id: ' + id);
|
||||
}
|
||||
}
|
||||
|
||||
static formatValue(type, value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (type == this.TYPE_INT) return Number(value);
|
||||
|
@ -214,7 +214,7 @@ function isAnchor(n) {
|
||||
}
|
||||
|
||||
function isIgnoredEndTag(n) {
|
||||
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n=='s' || n == 'tbody' || n == 'sup' || n == 'img' || n == 'abbr' || n == 'cite' || n == 'thead' || n == 'small' || n == 'tt' || n == 'sub';
|
||||
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n == 'tbody' || n == 'sup' || n == 'img' || n == 'abbr' || n == 'cite' || n == 'thead' || n == 'small' || n == 'tt' || n == 'sub';
|
||||
}
|
||||
|
||||
function isListTag(n) {
|
||||
@ -320,15 +320,6 @@ function enexXmlToMdArray(stream, resources) {
|
||||
type: 'table',
|
||||
lines: [],
|
||||
parent: section,
|
||||
toString: function() {
|
||||
let output = [];
|
||||
output.push(BLOCK_OPEN);
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
output = output.concat(this.lines[i].toMdLines());
|
||||
}
|
||||
output.push(BLOCK_CLOSE);
|
||||
return processMdArrayNewLines(output);
|
||||
},
|
||||
};
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
@ -345,17 +336,6 @@ function enexXmlToMdArray(stream, resources) {
|
||||
lines: [],
|
||||
parent: section,
|
||||
isHeader: false,
|
||||
// Normally tables are rendered properly as markdown, but for table within table within table... we cannot
|
||||
// handle this in Markdown so simply render it as one cell per line.
|
||||
toMdLines: function() {
|
||||
let output = [];
|
||||
output.push(BLOCK_OPEN);
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
output.push(this.lines[i].toString());
|
||||
}
|
||||
output.push(BLOCK_CLOSE);
|
||||
return output;
|
||||
},
|
||||
}
|
||||
|
||||
section.lines.push(newSection);
|
||||
@ -372,9 +352,6 @@ function enexXmlToMdArray(stream, resources) {
|
||||
type: 'td',
|
||||
lines: [],
|
||||
parent: section,
|
||||
toString: function() {
|
||||
return processMdArrayNewLines(this.lines);
|
||||
},
|
||||
};
|
||||
|
||||
section.lines.push(newSection);
|
||||
@ -564,6 +541,10 @@ function enexXmlToMdArray(stream, resources) {
|
||||
|
||||
if (section.lines.length < 1) throw new Error('Invalid anchor tag closing'); // Sanity check, but normally not possible
|
||||
|
||||
const pushEmptyAnchor = (url) => {
|
||||
section.lines.push('[link](' + url + ')');
|
||||
}
|
||||
|
||||
// When closing the anchor tag, check if there's is any text content. If not
|
||||
// put the URL as is (don't wrap it in [](url)). The markdown parser, using
|
||||
// GitHub flavour, will turn this URL into a link. This is to generate slightly
|
||||
@ -571,13 +552,38 @@ function enexXmlToMdArray(stream, resources) {
|
||||
let previous = section.lines[section.lines.length - 1];
|
||||
if (previous == '[') {
|
||||
section.lines.pop();
|
||||
section.lines.push(url);
|
||||
pushEmptyAnchor(url);
|
||||
} else if (!previous || previous == url) {
|
||||
section.lines.pop();
|
||||
section.lines.pop();
|
||||
section.lines.push(url);
|
||||
pushEmptyAnchor(url);
|
||||
} else {
|
||||
section.lines.push('](' + url + ')');
|
||||
// Need to remove any new line character between the current ']' and the previous '['
|
||||
// otherwise it won't render properly.
|
||||
let allSpaces = true;
|
||||
for (let i = section.lines.length - 1; i >= 0; i--) {
|
||||
const c = section.lines[i];
|
||||
if (c === '[') {
|
||||
break;
|
||||
} else {
|
||||
if (c === BLOCK_CLOSE || c === BLOCK_OPEN || c === NEWLINE) {
|
||||
section.lines[i] = SPACE;
|
||||
} else {
|
||||
if (!isWhiteSpace(c)) allSpaces = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allSpaces) {
|
||||
for (let i = section.lines.length - 1; i >= 0; i--) {
|
||||
const c = section.lines.pop();
|
||||
if (c === '[') break;
|
||||
}
|
||||
//section.lines.push(url);
|
||||
pushEmptyAnchor(url);
|
||||
} else {
|
||||
section.lines.push('](' + url + ')');
|
||||
}
|
||||
}
|
||||
} else if (isListTag(n)) {
|
||||
section.lines.push(BLOCK_CLOSE);
|
||||
@ -607,50 +613,28 @@ function enexXmlToMdArray(stream, resources) {
|
||||
});
|
||||
}
|
||||
|
||||
function setTableCellContent(table) {
|
||||
if (!table.type == 'table') throw new Error('Only for tables');
|
||||
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
let td = tr.lines[tdIndex];
|
||||
td.content = processMdArrayNewLines(td.lines);
|
||||
td.content = td.content.replace(/\n\n\n\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n\n\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n/g, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
function removeTableCellNewLines(cellText) {
|
||||
return cellText.replace(/\n+/g, " ");
|
||||
}
|
||||
|
||||
function cellWidth(cellText) {
|
||||
const lines = cellText.split("\n");
|
||||
let maxWidth = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.length > maxWidth) maxWidth = line.length;
|
||||
}
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
function colWidths(table) {
|
||||
let output = [];
|
||||
function tableHasSubTables(table) {
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
const td = tr.lines[tdIndex];
|
||||
const w = Math.min(cellWidth(td.content), 20); // Have to set a max width otherwise it can be extremely long for notes that import entire web pages (eg. Hacker News comment pages)
|
||||
if (output.length <= tdIndex) output.push(0);
|
||||
if (w > output[tdIndex]) output[tdIndex] = w;
|
||||
for (let i = 0; i < td.lines.length; i++) {
|
||||
if (typeof td.lines[i] === 'object') return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawTable(table, colWidths) {
|
||||
// Markdown tables don't support tables within tables, which is common in notes that are complete web pages, for example when imported
|
||||
// via Web Clipper. So to handle this, we render all the outer tables as regular text (as if replacing all the <table>, <tr> and <td>
|
||||
// elements by <div>) and only the inner ones, those that don't contain any other tables, are rendered as actual tables. This is generally
|
||||
// the required behaviour since the outer tables are usually for layout and the inner ones are the content.
|
||||
function drawTable(table) {
|
||||
// | First Header | Second Header |
|
||||
// | ------------- | ------------- |
|
||||
// | Content Cell | Content Cell |
|
||||
@ -658,8 +642,11 @@ function drawTable(table, colWidths) {
|
||||
|
||||
// There must be at least 3 dashes separating each header cell.
|
||||
// https://gist.github.com/IanWang/28965e13cdafdef4e11dc91f578d160d#tables
|
||||
|
||||
const flatRender = tableHasSubTables(table); // Render the table has regular text
|
||||
const minColWidth = 3;
|
||||
let lines = [];
|
||||
lines.push(BLOCK_OPEN);
|
||||
let headerDone = false;
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
@ -667,37 +654,79 @@ function drawTable(table, colWidths) {
|
||||
let line = [];
|
||||
let headerLine = [];
|
||||
let emptyHeader = null;
|
||||
for (let tdIndex = 0; tdIndex < colWidths.length; tdIndex++) {
|
||||
const width = Math.max(minColWidth, colWidths[tdIndex]);
|
||||
const cell = tr.lines[tdIndex] ? tr.lines[tdIndex].content : '';
|
||||
line.push(stringPadding(cell, width, ' ', stringPadding.RIGHT));
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
const td = tr.lines[tdIndex];
|
||||
|
||||
if (!headerDone) {
|
||||
if (!isHeader) {
|
||||
if (!emptyHeader) emptyHeader = [];
|
||||
let h = stringPadding(' ', width, ' ', stringPadding.RIGHT);
|
||||
if (!width) h = '';
|
||||
emptyHeader.push(h);
|
||||
if (flatRender) {
|
||||
line.push(BLOCK_OPEN);
|
||||
|
||||
let currentCells = [];
|
||||
|
||||
const renderCurrentCells = () => {
|
||||
if (!currentCells.length) return;
|
||||
const cellText = processMdArrayNewLines(currentCells);
|
||||
line.push(cellText);
|
||||
currentCells = [];
|
||||
}
|
||||
headerLine.push('-'.repeat(width));
|
||||
|
||||
// In here, recursively render the tables
|
||||
for (let i = 0; i < td.lines.length; i++) {
|
||||
const c = td.lines[i];
|
||||
if (typeof c === 'object') { // This is a table
|
||||
renderCurrentCells();
|
||||
currentCells = currentCells.concat(drawTable(c));
|
||||
} else { // This is plain text
|
||||
currentCells.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
renderCurrentCells();
|
||||
|
||||
line.push(BLOCK_CLOSE);
|
||||
} else { // Regular table rendering
|
||||
|
||||
// A cell in a Markdown table cannot have new lines so remove them
|
||||
const cellText = removeTableCellNewLines(processMdArrayNewLines(td.lines));
|
||||
|
||||
const width = Math.max(cellText.length, 3);
|
||||
line.push(stringPadding(cellText, width, ' ', stringPadding.RIGHT));
|
||||
|
||||
if (!headerDone) {
|
||||
if (!isHeader) {
|
||||
if (!emptyHeader) emptyHeader = [];
|
||||
let h = stringPadding(' ', width, ' ', stringPadding.RIGHT);
|
||||
emptyHeader.push(h);
|
||||
}
|
||||
headerLine.push('-'.repeat(width));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (emptyHeader) {
|
||||
lines.push('| ' + emptyHeader.join(' | ') + ' |');
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
if (flatRender) {
|
||||
headerDone = true;
|
||||
}
|
||||
lines.push(BLOCK_OPEN);
|
||||
lines = lines.concat(line);
|
||||
lines.push(BLOCK_CLOSE);
|
||||
} else {
|
||||
if (emptyHeader) {
|
||||
lines.push('| ' + emptyHeader.join(' | ') + ' |');
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
headerDone = true;
|
||||
}
|
||||
|
||||
lines.push('| ' + line.join(' | ') + ' |');
|
||||
lines.push('| ' + line.join(' | ') + ' |');
|
||||
|
||||
if (!headerDone) {
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
headerDone = true;
|
||||
if (!headerDone) {
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
headerDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('<<<<:D>>>>' + NEWLINE + '<<<<:D>>>>').split('<<<<:D>>>>');
|
||||
lines.push(BLOCK_CLOSE);
|
||||
|
||||
return flatRender ? lines : lines.join('<<<<:D>>>>' + NEWLINE + '<<<<:D>>>>').split('<<<<:D>>>>');
|
||||
}
|
||||
|
||||
async function enexXmlToMd(stream, resources) {
|
||||
@ -708,13 +737,9 @@ async function enexXmlToMd(stream, resources) {
|
||||
for (let i = 0; i < result.content.lines.length; i++) {
|
||||
let line = result.content.lines[i];
|
||||
if (typeof line === 'object') { // A table
|
||||
let table = setTableCellContent(line);
|
||||
//console.log(require('util').inspect(table, false, null))
|
||||
const cw = colWidths(table);
|
||||
const tableLines = drawTable(table, cw);
|
||||
mdLines.push(BLOCK_OPEN);
|
||||
const table = line;
|
||||
const tableLines = drawTable(table);
|
||||
mdLines = mdLines.concat(tableLines);
|
||||
mdLines.push(BLOCK_CLOSE);
|
||||
} else { // an actual line
|
||||
mdLines.push(line);
|
||||
}
|
||||
|
@ -222,9 +222,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
const body = await enexXmlToMd(contentStream, note.resources);
|
||||
delete note.bodyXml;
|
||||
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info('*************************************************************************');
|
||||
// console.info(body);
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info('*************************************************************************');
|
||||
|
||||
note.id = uuid.create();
|
||||
note.parent_id = parentFolderId;
|
||||
|
@ -218,15 +218,15 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.36/Joplin-Setup-0.10.36.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-Setup-0.10.37.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.36/Joplin-0.10.36.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-0.10.37.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.36/Joplin-0.10.36-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-0.10.37-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -309,7 +309,18 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
<p>Since this is still an actual URL, the terminal will still make it clickable. And with shorter URLs, the text is more readable and the links unlikely to be cut. Both resources (files that are attached to notes) and external links are handled in this way.</p>
|
||||
<h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
<p>In Markdown, links to resources are represented as a simple ID to the resource. In order to give access to these resources, they will be, like links, converted to local URLs. Clicking this link will then open a browser, which will handle the file - i.e. display the image, open the PDF file, etc.</p>
|
||||
<h1 id="available-shortcuts">Available shortcuts</h1>
|
||||
<h1 id="shell-mode">Shell mode</h1>
|
||||
<p>Commands can also be used directly from a shell. To view the list of available commands, type <code>joplin help all</code>. To reference a note, notebook or tag you can either use the ID (type <code>joplin ls -l</code> to view the ID) or by title.</p>
|
||||
<p>For example, this will create a new note "My note" in the notebook "My notebook":</p>
|
||||
<pre><code>$ joplin mkbook "My notebook"
|
||||
$ joplin use "My notebook"
|
||||
$ joplin mknote "My note"
|
||||
</code></pre><p>To view the newly created note:</p>
|
||||
<pre><code>$ joplin ls -l
|
||||
fe889 07/12/2017 17:57 My note
|
||||
</code></pre><p>Give a new title to the note:</p>
|
||||
<pre><code>$ joplin set fe889 title "New title"
|
||||
</code></pre><h1 id="available-shortcuts">Available shortcuts</h1>
|
||||
<p>There are two types of shortcuts: those that manipulate the user interface directly, such as <code>TAB</code> to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing <code>mn</code> ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with <code>mknote ""</code> from where the title of the note can be entered. See below for the full list of shortcuts:</p>
|
||||
<pre><code>Tab Give focus to next pane
|
||||
Shift+Tab Give focus to previous pane
|
||||
|
Loading…
Reference in New Issue
Block a user