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

All: Resolves #134: Allow linking to a note from another note

This commit is contained in:
Laurent Cozic 2018-05-02 15:13:20 +01:00
parent ff1ee1249b
commit a419bc7253
9 changed files with 125 additions and 49 deletions

View File

@ -96,6 +96,16 @@ class NoteListComponent extends React.Component {
}
}}));
menu.append(new MenuItem({label: _('Copy Markdown link'), click: async () => {
const { clipboard } = require('electron');
const links = [];
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
links.push(Note.markdownTag(note));
}
clipboard.writeText(links.join(' '));
}}));
const exportMenu = new Menu();
const ioService = new InteropService();

View File

@ -1,5 +1,6 @@
const React = require('react');
const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js');
const Search = require('lib/models/Search.js');
const { time } = require('lib/time-utils.js');
@ -76,7 +77,6 @@ class NoteTextComponent extends React.Component {
mdToHtml() {
if (this.mdToHtml_) return this.mdToHtml_;
this.mdToHtml_ = new MdToHtml({
supportsResourceLinks: true,
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
});
return this.mdToHtml_;
@ -334,11 +334,29 @@ class NoteTextComponent extends React.Component {
menu.popup(bridge().window());
} else if (msg.indexOf('joplin://') === 0) {
const resourceId = msg.substr('joplin://'.length);
Resource.load(resourceId).then((resource) => {
const filePath = Resource.fullPath(resource);
const itemId = msg.substr('joplin://'.length);
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error('No item with ID ' + itemId);
if (item.type_ === BaseModel.TYPE_RESOURCE) {
const filePath = Resource.fullPath(item);
bridge().openItem(filePath);
});
} else if (item.type_ === BaseModel.TYPE_NOTE) {
this.props.dispatch({
type: "FOLDER_SELECT",
id: item.parent_id,
});
setTimeout(() => {
this.props.dispatch({
type: 'NOTE_SELECT',
id: item.id,
});
}, 10);
} else {
throw new Error('Unsupported item type: ' + item.type_);
}
} else {
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
}

View File

@ -203,6 +203,14 @@ If for any reason the notifications do not work, please [open an issue](https://
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
## Links to other notes
You can create a link to a note by specifying its ID in the URL. For example:
[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the **desktop app**, right click on a note an select "Copy Markdown link". In the **mobile app**, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.
## Math notation
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow:

View File

@ -14,7 +14,6 @@ class MdToHtml {
constructor(options = null) {
if (!options) options = {};
this.supportsResourceLinks_ = !!options.supportsResourceLinks;
this.loadedResources_ = {};
this.cachedContent_ = null;
this.cachedContentKey_ = null;
@ -132,40 +131,27 @@ class MdToHtml {
const isResourceUrl = Resource.isResourceUrl(href);
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
if (isResourceUrl && !this.supportsResourceLinks_) {
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
// Ideally they should be opened in the user's browser.
return '<span style="opacity: 0.5">(Resource not yet supported: ';
let resourceIdAttr = "";
let icon = "";
let hrefAttr = '#';
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} else {
let resourceIdAttr = "";
let icon = "";
let hrefAttr = '#';
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} else {
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
return output;
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
return output;
}
renderCloseLink_(attrs, options) {
const href = this.getAttr_(attrs, 'href');
const isResourceUrl = Resource.isResourceUrl(href);
if (isResourceUrl && !this.supportsResourceLinks_) {
return ')</span>';
} else {
return '</a>';
}
return '</a>';
}
rendererPlugin_(language) {

View File

@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { Platform, WebView, View, Linking } = require('react-native');
const { Platform, WebView, View } = require('react-native');
const { globalStyle } = require('lib/components/global-style.js');
const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js');
@ -19,7 +19,7 @@ class NoteBodyViewer extends Component {
}
UNSAFE_componentWillMount() {
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: false });
this.mdToHtml_ = new MdToHtml();
this.isMounted_ = true;
}
@ -115,7 +115,7 @@ class NoteBodyViewer extends Component {
//msg = msg.split(':');
//this.bodyScrollTop_ = Number(msg[1]);
} else {
Linking.openURL(msg);
this.props.onJoplinLinkClick(msg);
}
}}
/>

View File

@ -1,9 +1,10 @@
const React = require('react'); const Component = React.Component;
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
const { Platform, Clipboard, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
const { connect } = require('react-redux');
const { uuid } = require('lib/uuid.js');
const RNFS = require('react-native-fs');
const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
const Setting = require('lib/models/Setting.js');
const Resource = require('lib/models/Resource.js');
const Folder = require('lib/models/Folder.js');
@ -22,8 +23,8 @@ const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
const { shim } = require('lib/shim.js');
const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { dialogs } = require('lib/dialogs.js');
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
const { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
const { NoteBodyViewer } = require('lib/components/note-body-viewer.js');
const RNFetchBlob = require('react-native-fetch-blob').default;
@ -107,6 +108,39 @@ class NoteScreenComponent extends BaseScreenComponent {
this.noteTagDialog_closeRequested = () => {
this.setState({ noteTagDialogShown: false });
}
this.onJoplinLinkClick_ = async (msg) => {
try {
if (msg.indexOf('joplin://') === 0) {
const itemId = msg.substr('joplin://'.length);
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error(_('No item with ID %s', itemId));
if (item.type_ === BaseModel.TYPE_NOTE) {
// Easier to just go back, then go to the note since
// the Note screen doesn't handle reloading a different note
this.props.dispatch({
type: 'NAV_BACK',
});
setTimeout(() => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Note',
noteId: item.id,
});
}, 5);
} else {
throw new Error(_('The Joplin mobile app does not currently support this type of link: %s', BaseModel.modelTypeToName(item.type_)));
}
} else {
Linking.openURL(msg);
}
} catch (error) {
dialogs.error(this, error.message);
}
}
}
styles() {
@ -402,6 +436,11 @@ class NoteScreenComponent extends BaseScreenComponent {
}
}
copyMarkdownLink_onPress() {
const note = this.state.note;
Clipboard.setString(Note.markdownTag(note));
}
menuOptions() {
const note = this.state.note;
const isTodo = note && !!note.is_todo;
@ -425,6 +464,7 @@ class NoteScreenComponent extends BaseScreenComponent {
if (isSaved) output.push({ title: _('Tags'), onPress: () => { this.tags_onPress(); } });
output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
if (isSaved) output.push({ title: _('Copy Markdown link'), onPress: () => { this.copyMarkdownLink_onPress(); } });
output.push({ isDivider: true });
if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } });
@ -466,7 +506,7 @@ class NoteScreenComponent extends BaseScreenComponent {
this.saveOneProperty('body', newBody);
};
bodyComponent = <NoteBodyViewer style={this.styles().noteBodyViewer} webViewStyle={theme} note={note} onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}/>
bodyComponent = <NoteBodyViewer onJoplinLinkClick={this.onJoplinLinkClick_} style={this.styles().noteBodyViewer} webViewStyle={theme} note={note} onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}/>
} else {
const focusBody = !isNew && !!note.title;

View File

@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js');
const { _ } = require('lib/locale.js');
const moment = require('moment');
const { markdownUtils } = require('lib/markdown-utils.js');
class BaseItem extends BaseModel {
@ -649,6 +650,15 @@ class BaseItem extends BaseModel {
return super.save(o, options);
}
static markdownTag(item) {
const output = [];
output.push('[');
output.push(markdownUtils.escapeLinkText(item.title));
output.push(']');
output.push('(:/' + item.id + ')');
return output.join('');
}
}
BaseItem.encryptionService_ = null;

View File

@ -45,7 +45,7 @@ function createChangeLog(releases) {
let s = [];
s.push('## ' + r.tag_name + ' - ' + r.published_at);
s.push('');
let body = r.body.replace(/(#\d+)/g, '[$1](https://github.com/laurent22/joplin/issues/$1)');
let body = r.body.replace(/#(\d+)/g, '[$1](https://github.com/laurent22/joplin/issues/$1)');
s.push(body);
output.push(s.join('\n'));
}

View File

@ -422,6 +422,10 @@
<p>If for any reason the notifications do not work, please <a href="https://github.com/laurent22/joplin/issues">open an issue</a>.</p>
<h1 id="markdown">Markdown</h1>
<p>Joplin uses and renders <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet">Github-flavoured Markdown</a> with a few variations and additions. In particular:</p>
<h2 id="links-to-other-notes">Links to other notes</h2>
<p>You can create a link to a note by specifying its ID in the URL. For example:</p>
<pre><code>[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
</code></pre><p>Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the <strong>desktop app</strong>, right click on a note an select &quot;Copy Markdown link&quot;. In the <strong>mobile app</strong>, open a note and, in the top right menu, select &quot;Copy Markdown link&quot;. You can then paste this link anywhere in another note.</p>
<h2 id="math-notation">Math notation</h2>
<p>Math expressions can be added using the <a href="https://khan.github.io/KaTeX/">Katex notation</a>. To add an inline equation, wrap the expression in <code>$EXPRESSION$</code>, eg. <code>$\sqrt{3x-1}+(1+x)^2$</code>. To create an expression block, wrap it as follow:</p>
<pre><code>$$
@ -486,14 +490,14 @@ $$
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/hr.png" alt=""></td>
<td>Croatian</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
<td>Hrvoje Mandić <a href="&#109;&#97;&#x69;&#x6c;&#x74;&#111;&#x3a;&#116;&#x72;&#98;&#x75;&#104;&#x6f;&#109;&#x40;&#110;&#101;&#116;&#x2e;&#104;&#x72;">&#116;&#x72;&#98;&#x75;&#104;&#x6f;&#109;&#x40;&#110;&#101;&#116;&#x2e;&#104;&#x72;</a></td>
<td>Hrvoje Mandić <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x74;&#x72;&#98;&#117;&#x68;&#x6f;&#109;&#x40;&#110;&#x65;&#x74;&#46;&#104;&#x72;">&#x74;&#x72;&#98;&#117;&#x68;&#x6f;&#109;&#x40;&#110;&#x65;&#x74;&#46;&#104;&#x72;</a></td>
<td>62%</td>
</tr>
<tr>
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/cz.png" alt=""></td>
<td>Czech</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po">cs_CZ</a></td>
<td>Lukas Helebrandt <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#x6c;&#117;&#107;&#97;&#115;&#64;&#97;&#x69;&#x79;&#x61;&#x2e;&#99;&#x7a;">&#x6c;&#117;&#107;&#97;&#115;&#64;&#97;&#x69;&#x79;&#x61;&#x2e;&#99;&#x7a;</a></td>
<td>Lukas Helebrandt <a href="&#x6d;&#97;&#105;&#108;&#116;&#111;&#x3a;&#x6c;&#x75;&#x6b;&#97;&#x73;&#64;&#x61;&#x69;&#121;&#x61;&#x2e;&#x63;&#122;">&#x6c;&#x75;&#x6b;&#97;&#x73;&#64;&#x61;&#x69;&#121;&#x61;&#x2e;&#x63;&#122;</a></td>
<td>96%</td>
</tr>
<tr>
@ -507,7 +511,7 @@ $$
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/de.png" alt=""></td>
<td>Deutsch</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
<td>Tobias Grasse <a href="&#x6d;&#x61;&#105;&#108;&#x74;&#x6f;&#x3a;&#x6d;&#97;&#105;&#108;&#x40;&#116;&#x6f;&#98;&#105;&#97;&#x73;&#x2d;&#103;&#x72;&#97;&#115;&#115;&#x65;&#x2e;&#x6e;&#x65;&#x74;">&#x6d;&#97;&#105;&#108;&#x40;&#116;&#x6f;&#98;&#105;&#97;&#x73;&#x2d;&#103;&#x72;&#97;&#115;&#115;&#x65;&#x2e;&#x6e;&#x65;&#x74;</a></td>
<td>Tobias Grasse <a href="&#109;&#97;&#x69;&#x6c;&#116;&#111;&#58;&#109;&#97;&#105;&#108;&#64;&#116;&#x6f;&#98;&#x69;&#x61;&#115;&#x2d;&#103;&#x72;&#x61;&#x73;&#x73;&#101;&#46;&#110;&#x65;&#116;">&#109;&#97;&#105;&#108;&#64;&#116;&#x6f;&#98;&#x69;&#x61;&#115;&#x2d;&#103;&#x72;&#x61;&#x73;&#x73;&#101;&#46;&#110;&#x65;&#116;</a></td>
<td>95%</td>
</tr>
<tr>
@ -521,7 +525,7 @@ $$
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/es.png" alt=""></td>
<td>Español</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
<td>Fernando Martín <a href="&#109;&#97;&#105;&#108;&#x74;&#111;&#x3a;&#102;&#64;&#x6d;&#114;&#116;&#110;&#x2e;&#x65;&#115;">&#102;&#64;&#x6d;&#114;&#116;&#110;&#x2e;&#x65;&#115;</a></td>
<td>Fernando Martín <a href="&#109;&#x61;&#x69;&#108;&#116;&#111;&#x3a;&#102;&#x40;&#109;&#114;&#x74;&#x6e;&#46;&#x65;&#x73;">&#102;&#x40;&#109;&#114;&#x74;&#x6e;&#46;&#x65;&#x73;</a></td>
<td>99%</td>
</tr>
<tr>
@ -535,7 +539,7 @@ $$
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/es.png" alt=""></td>
<td>Galician</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po">gl_ES</a></td>
<td>Marcos Lans <a href="&#x6d;&#97;&#105;&#108;&#116;&#x6f;&#58;&#109;&#x61;&#x72;&#x63;&#111;&#x73;&#x6c;&#97;&#x6e;&#115;&#103;&#97;&#x72;&#122;&#97;&#64;&#103;&#x6d;&#x61;&#x69;&#108;&#x2e;&#x63;&#111;&#109;">&#109;&#x61;&#x72;&#x63;&#111;&#x73;&#x6c;&#97;&#x6e;&#115;&#103;&#97;&#x72;&#122;&#97;&#64;&#103;&#x6d;&#x61;&#x69;&#108;&#x2e;&#x63;&#111;&#109;</a></td>
<td>Marcos Lans <a href="&#109;&#97;&#105;&#108;&#116;&#x6f;&#x3a;&#x6d;&#97;&#114;&#x63;&#x6f;&#115;&#x6c;&#97;&#x6e;&#x73;&#103;&#x61;&#114;&#122;&#x61;&#64;&#103;&#109;&#x61;&#105;&#x6c;&#x2e;&#x63;&#x6f;&#109;">&#x6d;&#97;&#114;&#x63;&#x6f;&#115;&#x6c;&#97;&#x6e;&#x73;&#103;&#x61;&#114;&#122;&#x61;&#64;&#103;&#109;&#x61;&#105;&#x6c;&#x2e;&#x63;&#x6f;&#109;</a></td>
<td>97%</td>
</tr>
<tr>
@ -556,14 +560,14 @@ $$
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/br.png" alt=""></td>
<td>Português (Brasil)</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po">pt_BR</a></td>
<td>Renato Nunes Bastos <a href="&#109;&#x61;&#x69;&#108;&#116;&#x6f;&#58;&#114;&#110;&#x62;&#97;&#115;&#x74;&#x6f;&#115;&#x40;&#103;&#109;&#97;&#105;&#x6c;&#46;&#x63;&#x6f;&#109;">&#114;&#110;&#x62;&#97;&#115;&#x74;&#x6f;&#115;&#x40;&#103;&#109;&#97;&#105;&#x6c;&#46;&#x63;&#x6f;&#109;</a></td>
<td>Renato Nunes Bastos <a href="&#109;&#x61;&#x69;&#108;&#116;&#x6f;&#58;&#114;&#110;&#98;&#x61;&#115;&#x74;&#x6f;&#115;&#x40;&#x67;&#x6d;&#x61;&#105;&#x6c;&#46;&#99;&#x6f;&#x6d;">&#114;&#110;&#98;&#x61;&#115;&#x74;&#x6f;&#115;&#x40;&#x67;&#x6d;&#x61;&#105;&#x6c;&#46;&#99;&#x6f;&#x6d;</a></td>
<td>99%</td>
</tr>
<tr>
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/ru.png" alt=""></td>
<td>Русский</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
<td>Artyom Karlov <a href="&#x6d;&#97;&#105;&#108;&#x74;&#111;&#58;&#97;&#114;&#116;&#121;&#x6f;&#x6d;&#x2e;&#107;&#x61;&#114;&#108;&#111;&#x76;&#x40;&#x67;&#x6d;&#97;&#x69;&#108;&#46;&#99;&#x6f;&#109;">&#97;&#114;&#116;&#121;&#x6f;&#x6d;&#x2e;&#107;&#x61;&#114;&#108;&#111;&#x76;&#x40;&#x67;&#x6d;&#97;&#x69;&#108;&#46;&#99;&#x6f;&#109;</a></td>
<td>Artyom Karlov <a href="&#x6d;&#97;&#105;&#x6c;&#116;&#x6f;&#x3a;&#x61;&#114;&#x74;&#121;&#111;&#109;&#x2e;&#107;&#97;&#x72;&#108;&#111;&#118;&#64;&#x67;&#x6d;&#x61;&#x69;&#x6c;&#x2e;&#x63;&#x6f;&#109;">&#x61;&#114;&#x74;&#121;&#111;&#109;&#x2e;&#107;&#97;&#x72;&#108;&#111;&#118;&#64;&#x67;&#x6d;&#x61;&#x69;&#x6c;&#x2e;&#x63;&#x6f;&#109;</a></td>
<td>95%</td>
</tr>
<tr>