You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
43 Commits
android-v2
...
plugin-gen
Author | SHA1 | Date | |
---|---|---|---|
|
15394a3f7e | ||
|
c3cdeeb315 | ||
|
af882612e9 | ||
|
11fffc0e4e | ||
|
ea2d9ca467 | ||
|
e027cdce26 | ||
|
229b9a580e | ||
|
a6df9f119c | ||
|
6670f9ae1c | ||
|
09506a7b40 | ||
|
973193623a | ||
|
495f088320 | ||
|
d264bdd14d | ||
|
144ec1eea2 | ||
|
d4157e14fe | ||
|
815a0a5db4 | ||
|
1bbec445d5 | ||
|
fd5a4dcbbf | ||
|
604dcbc35b | ||
|
b459ba7224 | ||
|
0d0398312f | ||
|
39c8fc812d | ||
|
0638d711d7 | ||
|
9bad668cc5 | ||
|
c18c31ab7f | ||
|
7c24a2f4be | ||
|
56438ea644 | ||
|
7f9bc1e15c | ||
|
b1c8cb5632 | ||
|
f0a1b41794 | ||
|
02982464a6 | ||
|
62e317db05 | ||
|
e0795748a9 | ||
|
67070ed3d5 | ||
|
fec8c6131c | ||
|
24ed5bda63 | ||
|
dbb354ad10 | ||
|
92dccbe98d | ||
|
9b775d77f6 | ||
|
4fd6937d05 | ||
|
7230f0e698 | ||
|
6cd0938ee4 | ||
|
c3dc30ee5d |
@@ -385,6 +385,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
@@ -542,6 +543,7 @@ packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -367,6 +367,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
@@ -524,6 +525,7 @@ packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
|
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
@@ -291,4 +291,29 @@ describe('MdToHtml', () => {
|
||||
expect(html.html).toContain(opening + trimmedTex + closing);
|
||||
}
|
||||
});
|
||||
|
||||
it('should render inline KaTeX after a numbered equation', async () => {
|
||||
const mdToHtml = newTestMdToHtml();
|
||||
|
||||
// This test is intended to verify that inline KaTeX renders correctly
|
||||
// after creating a numbered equation with \begin{align}...\end{align}.
|
||||
//
|
||||
// See https://github.com/laurent22/joplin/issues/9455 for details.
|
||||
|
||||
const markdown = [
|
||||
'$$',
|
||||
'\\begin{align}\\text{Block}\\end{align}',
|
||||
'$$',
|
||||
'',
|
||||
'$\\text{Inline}$',
|
||||
].join('\n');
|
||||
const { html } = await mdToHtml.render(markdown, null, { bodyOnly: true });
|
||||
|
||||
// Because we don't control the output of KaTeX, this test should be as general as
|
||||
// possible while still verifying that rendering (without an error) occurs.
|
||||
|
||||
// Should have rendered the inline and block content without errors
|
||||
expect(html).toContain('Inline</span>');
|
||||
expect(html).toContain('Block</span>');
|
||||
});
|
||||
});
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<en-note><div>Plain note</div></en-note>]]></content><created>20201223T163948Z</created><updated>20201223T163953Z</updated><note-attributes><author>laurent22777@gmail.com</author><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes></note><note><title>Note 2</title><content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
||||
|
||||
<en-note><div><div><br/></div><table style="border-collapse: collapse; min-width: 100%;"><colgroup><col style="width: 130px;"/><col style="width: 130px;"/><col style="width: 130px;"/></colgroup><tbody><tr<td style="width: 130px; padding: 8px; border: 1px solid;"><div>test</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div>test</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div><br/></div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>bl</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div>bla</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div>bla</div></td></tr></tbody></table><div><br/></div></div><div><br/></div></en-note>]]></content><created>20201223T164010Z</created><updated>20201223T164023Z</updated><note-attributes><author>laurent22777@gmail.com</author><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes></note><note><title>plain note 2</title><content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
|
||||
<en-note><div><div><br/></div><table style="border-collapse: collapse; min-width: 100%;"><colgroup><col style="width: 130px;"/><col style="width: 130px;"/><col style="width: 130px;"/></colgroup><tbody><tr<td style="width: 130px; padding: 8px; border: 1px solid;"><div>test</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div>test</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div><br/></div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>bl</div></t><td style="width: 130px; padding: 8px; border: 1px solid;"><div>bla</div></td><td style="width: 130px; padding: 8px; border: 1px solid;"><div>bla</div></td></tr></tbody></table><div><br/></div></div><div><br/></div></en-note>]]></content><created>20201223T164010Z</created><updated>20201223T164023Z</updated><note-attributes><author>laurent22777@gmail.com</author><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes></note><note><title>plain note 2</title><content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
||||
|
||||
<en-note><div><br/></div></en-note>]]></content><created>20201223T164236Z</created><note-attributes><author>laurent22777@gmail.com</author><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes></note></en-export>
|
||||
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
<table>
|
||||
<div></div> <!-- INVALID! -->
|
||||
<tr>
|
||||
<td>one</td>
|
||||
<td>two</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<div></div> <!-- INVALID! -->
|
||||
<td>three</td>
|
||||
<td>four</td>
|
||||
</tr>
|
||||
</table>
|
@@ -0,0 +1,4 @@
|
||||
| | |
|
||||
| --- | --- |
|
||||
| one | two |
|
||||
| three | four |
|
@@ -1,10 +1,10 @@
|
||||
<body>
|
||||
<table border="5px" bordercolor="#8707B0">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Left side of the main table</td>
|
||||
<td>
|
||||
<table border="5px" bordercolor="#F35557">
|
||||
<h4 align="center">Nested Table</h4>
|
||||
<table>
|
||||
<b>Nested Table</b>
|
||||
<tr>
|
||||
<td>nested table C1</td>
|
||||
<td>nested table C2</td>
|
||||
|
@@ -1 +1 @@
|
||||
<table border="5px" bordercolor="#8707B0"><tbody><tr><td>Left side of the main table</td><td><h4 align="center">Nested Table</h4><table border="5px" bordercolor="#F35557"><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table>
|
||||
<table><tbody><tr><td>Left side of the main table</td><td><b>Nested Table</b><table><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table>
|
@@ -856,7 +856,18 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
const resourcesEqual = resourceInfosEqual(lastOnChangeEventInfo.current.resourceInfos, props.resourceInfos);
|
||||
|
||||
if (lastOnChangeEventInfo.current.content !== props.content || !resourcesEqual) {
|
||||
const result = await props.markupToHtml(props.contentMarkupLanguage, props.content, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
||||
const result = await props.markupToHtml(
|
||||
props.contentMarkupLanguage,
|
||||
props.content,
|
||||
markupRenderOptions({
|
||||
resourceInfos: props.resourceInfos,
|
||||
|
||||
// Allow file:// URLs that point to the resource directory.
|
||||
// This prevents HTML-style resource URLs (e.g. <a href="file://path/to/resource/.../"></a>)
|
||||
// from being discarded.
|
||||
allowedFilePrefixes: [props.resourceDirectory],
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
|
||||
editor.setContent(awfulBrHack(result.html));
|
||||
|
@@ -439,6 +439,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
contentMarkupLanguage: formNote.markup_language,
|
||||
contentOriginalCss: formNote.originalCss,
|
||||
resourceInfos: resourceInfos,
|
||||
resourceDirectory: Setting.value('resourceDir'),
|
||||
htmlToMarkdown: htmlToMarkdown,
|
||||
markupToHtml: markupToHtml,
|
||||
allAssets: allAssets,
|
||||
|
@@ -82,6 +82,7 @@ export interface NoteBodyEditorProps {
|
||||
visiblePanes: string[];
|
||||
keyboardMode: string;
|
||||
resourceInfos: ResourceInfos;
|
||||
resourceDirectory: string;
|
||||
locale: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onDrop: Function;
|
||||
|
@@ -26,6 +26,7 @@ export interface MarkupToHtmlOptions {
|
||||
noteId?: string;
|
||||
vendorDir?: string;
|
||||
platformName?: string;
|
||||
allowedFilePrefixes?: string[];
|
||||
}
|
||||
|
||||
export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
|
@@ -7,6 +7,7 @@ import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import createStartupArgs from './util/createStartupArgs';
|
||||
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
@@ -20,17 +21,7 @@ test.describe('main', () => {
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.newNoteButton.click();
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.waitFor();
|
||||
|
||||
// Wait for the title input to have the correct placeholder
|
||||
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
|
||||
|
||||
// Fill the title
|
||||
await editor.noteTitleInput.click();
|
||||
await editor.noteTitleInput.fill('Test note');
|
||||
const editor = await mainScreen.createNewNote('Test note');
|
||||
|
||||
// Note list should contain the new note
|
||||
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
|
||||
@@ -49,6 +40,50 @@ test.describe('main', () => {
|
||||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
|
||||
});
|
||||
|
||||
test('HTML links should be preserved when editing a note in the WYSIWYG editor', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.createNewNote('Testing!');
|
||||
const editor = mainScreen.noteEditor;
|
||||
|
||||
// Set the note's content
|
||||
await editor.codeMirrorEditor.click();
|
||||
|
||||
// Attach this file to the note (create a resource ID)
|
||||
await setFilePickerResponse(electronApp, [__filename]);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
// Wait to render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
await viewerFrame.locator('a[data-from-md]').waitFor();
|
||||
|
||||
// Should have an attached resource
|
||||
const codeMirrorContent = await editor.codeMirrorEditor.innerText();
|
||||
|
||||
const resourceUrlExpression = /\[.*\]\(:\/(\w+)\)/;
|
||||
expect(codeMirrorContent).toMatch(resourceUrlExpression);
|
||||
const resourceId = codeMirrorContent.match(resourceUrlExpression)[1];
|
||||
|
||||
// Create a new note with just an HTML link
|
||||
await mainScreen.createNewNote('Another test');
|
||||
await editor.codeMirrorEditor.click();
|
||||
await mainWindow.keyboard.type(`<a href=":/${resourceId}">HTML Link</a>`);
|
||||
|
||||
// Switch to the RTE
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.richTextEditor.waitFor();
|
||||
|
||||
// Edit the note to cause the original content to update
|
||||
await editor.getTinyMCEFrameLocator().locator('a').click();
|
||||
await mainWindow.keyboard.type('Test...');
|
||||
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.codeMirrorEditor.waitFor();
|
||||
|
||||
// Note should still contain the resource ID and note title
|
||||
const finalCodeMirrorContent = await editor.codeMirrorEditor.innerText();
|
||||
expect(finalCodeMirrorContent).toContain(`:/${resourceId}`);
|
||||
});
|
||||
|
||||
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
@@ -6,7 +6,7 @@ export default class MainScreen {
|
||||
public readonly noteListContainer: Locator;
|
||||
public readonly noteEditor: NoteEditorScreen;
|
||||
|
||||
public constructor(page: Page) {
|
||||
public constructor(private page: Page) {
|
||||
this.newNoteButton = page.locator('.new-note-button');
|
||||
this.noteListContainer = page.locator('.rli-noteList');
|
||||
this.noteEditor = new NoteEditorScreen(page);
|
||||
@@ -17,4 +17,20 @@ export default class MainScreen {
|
||||
await this.noteEditor.waitFor();
|
||||
await this.noteListContainer.waitFor();
|
||||
}
|
||||
|
||||
// Follows the steps a user would use to create a new note.
|
||||
public async createNewNote(title: string) {
|
||||
await this.waitFor();
|
||||
await this.newNoteButton.click();
|
||||
await this.noteEditor.waitFor();
|
||||
|
||||
// Wait for the title input to have the correct placeholder
|
||||
await this.page.locator('input[placeholder^="Creating new note"]').waitFor();
|
||||
|
||||
// Fill the title
|
||||
await this.noteEditor.noteTitleInput.click();
|
||||
await this.noteEditor.noteTitleInput.fill(title);
|
||||
|
||||
return this.noteEditor;
|
||||
}
|
||||
}
|
||||
|
@@ -3,13 +3,19 @@ import { Locator, Page } from '@playwright/test';
|
||||
|
||||
export default class NoteEditorPage {
|
||||
public readonly codeMirrorEditor: Locator;
|
||||
public readonly richTextEditor: Locator;
|
||||
public readonly noteTitleInput: Locator;
|
||||
public readonly attachFileButton: Locator;
|
||||
public readonly toggleEditorsButton: Locator;
|
||||
private readonly containerLocator: Locator;
|
||||
|
||||
public constructor(private readonly page: Page) {
|
||||
this.containerLocator = page.locator('.rli-editor');
|
||||
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
|
||||
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||
this.attachFileButton = this.containerLocator.locator('[title^="Attach file"]');
|
||||
this.toggleEditorsButton = this.containerLocator.locator('[title^="Toggle editors"]');
|
||||
}
|
||||
|
||||
public getNoteViewerIframe() {
|
||||
@@ -19,8 +25,15 @@ export default class NoteEditorPage {
|
||||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
|
||||
}
|
||||
|
||||
public getTinyMCEFrameLocator() {
|
||||
// We use frameLocator(':scope') to convert the richTextEditor Locator into
|
||||
// a FrameLocator. (:scope selects the locator itself).
|
||||
// https://playwright.dev/docs/api/class-framelocator
|
||||
return this.richTextEditor.frameLocator(':scope');
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.codeMirrorEditor.waitFor();
|
||||
await this.noteTitleInput.waitFor();
|
||||
await this.toggleEditorsButton.waitFor();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,13 @@
|
||||
import { ElectronApplication } from '@playwright/test';
|
||||
|
||||
const setFilePickerResponse = (electronApp: ElectronApplication, response: string[]) => {
|
||||
return electronApp.evaluate(async ({ dialog }, response) => {
|
||||
dialog.showOpenDialog = async () => ({
|
||||
canceled: false,
|
||||
filePaths: response,
|
||||
});
|
||||
dialog.showOpenDialogSync = () => response;
|
||||
}, response);
|
||||
};
|
||||
|
||||
export default setFilePickerResponse;
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.13.6",
|
||||
"version": "2.13.13",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -110,8 +110,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097727
|
||||
versionName "2.13.7"
|
||||
versionCode 2097730
|
||||
versionName "2.13.10"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
const React = require('react');
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Dispatch } from 'redux';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
// eslint-disable-next-line no-undef -- Don't know why it says React is undefined when it's defined above
|
||||
type FABGroupProps = React.ComponentProps<typeof FAB.Group>;
|
||||
|
||||
type OnButtonPress = ()=> void;
|
||||
interface ButtonSpec {
|
||||
@@ -19,6 +21,7 @@ interface ActionButtonProps {
|
||||
|
||||
// If not given, an "add" button will be used.
|
||||
mainButton?: ButtonSpec;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
const defaultOnPress = () => {};
|
||||
@@ -36,10 +39,12 @@ const useIcon = (iconName: string) => {
|
||||
|
||||
const ActionButton = (props: ActionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onMenuToggled = useCallback(
|
||||
(state: { open: boolean }) => setOpen(state.open)
|
||||
, [setOpen]);
|
||||
|
||||
const onMenuToggled: FABGroupProps['onStateChange'] = useCallback(state => {
|
||||
props.dispatch({
|
||||
type: 'SIDE_MENU_CLOSE',
|
||||
});
|
||||
setOpen(state.open);
|
||||
}, [setOpen, props.dispatch]);
|
||||
|
||||
const actions = useMemo(() => (props.buttons ?? []).map(button => {
|
||||
return {
|
||||
|
@@ -156,10 +156,6 @@ export const createJsDrawEditor = (
|
||||
// Load from a template if no initial data
|
||||
if (svgData === '') {
|
||||
await applyTemplateToEditor(editor, templateData);
|
||||
|
||||
// The editor expects to be saved initially (without
|
||||
// unsaved changes). Save now.
|
||||
saveNow();
|
||||
} else {
|
||||
await editor.loadFromSVG(svgData);
|
||||
}
|
||||
|
@@ -270,6 +270,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
|
||||
const setInitialSelectionJS = props.initialSelection ? `
|
||||
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
|
||||
cm.execCommand('scrollSelectionIntoView');
|
||||
` : '';
|
||||
|
||||
const editorSettings: EditorSettings = {
|
||||
@@ -331,6 +332,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
const settings = ${JSON.stringify(editorSettings)};
|
||||
|
||||
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, settings);
|
||||
|
||||
${setInitialSelectionJS}
|
||||
|
||||
window.onresize = () => {
|
||||
|
@@ -40,9 +40,7 @@ interface ActionButtonProps {
|
||||
onPress: Callback;
|
||||
}
|
||||
|
||||
const ActionButton = (
|
||||
props: ActionButtonProps,
|
||||
) => {
|
||||
const ActionButton = (props: ActionButtonProps) => {
|
||||
return (
|
||||
<CustomButton
|
||||
themeId={props.themeId}
|
||||
|
@@ -10,6 +10,7 @@ import NoteEditor from '../NoteEditor/NoteEditor';
|
||||
const FileViewer = require('react-native-file-viewer').default;
|
||||
const React = require('react');
|
||||
const { Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
||||
import type { NativeSyntheticEvent } from 'react-native';
|
||||
import { Platform, PermissionsAndroid } from 'react-native';
|
||||
const { connect } = require('react-redux');
|
||||
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
|
||||
@@ -50,8 +51,9 @@ import isEditableResource from '../NoteEditor/ImageEditor/isEditableResource';
|
||||
import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog';
|
||||
import { voskEnabled } from '../../services/voiceTyping/vosk';
|
||||
import { isSupportedLanguage } from '../../services/voiceTyping/vosk.android';
|
||||
import { ChangeEvent as EditorChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { join } from 'path';
|
||||
import { SelectionRange } from '../NoteEditor/types';
|
||||
const urlUtils = require('@joplin/lib/urlUtils');
|
||||
|
||||
// import Vosk from 'react-native-vosk';
|
||||
@@ -64,6 +66,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// This isn't in this.state because we don't want changing scroll to trigger
|
||||
// a re-render.
|
||||
private lastBodyScroll: number|undefined = undefined;
|
||||
private selection: SelectionRange;
|
||||
|
||||
public static navigationOptions(): any {
|
||||
return { header: null };
|
||||
@@ -251,7 +254,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this);
|
||||
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
|
||||
this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this);
|
||||
this.body_selectionChange = this.body_selectionChange.bind(this);
|
||||
this.onBodyViewerLoadEnd = this.onBodyViewerLoadEnd.bind(this);
|
||||
this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this);
|
||||
this.onBodyChange = this.onBodyChange.bind(this);
|
||||
@@ -520,13 +522,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
private body_selectionChange(event: any) {
|
||||
if (this.useEditorBeta()) {
|
||||
this.selection = event.selection;
|
||||
} else {
|
||||
this.selection = event.nativeEvent.selection;
|
||||
}
|
||||
}
|
||||
private onPlainEdtiorSelectionChange = (event: NativeSyntheticEvent<any>) => {
|
||||
this.selection = event.nativeEvent.selection;
|
||||
};
|
||||
|
||||
private onMarkdownEditorSelectionChange = (event: SelectionRangeChangeEvent) => {
|
||||
this.selection = { start: event.from, end: event.to };
|
||||
};
|
||||
|
||||
public makeSaveAction() {
|
||||
return async () => {
|
||||
@@ -749,7 +751,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (this.useEditorBeta()) {
|
||||
// The beta editor needs to be explicitly informed of changes
|
||||
// to the note's body
|
||||
this.editorRef.current.insertText(newText);
|
||||
if (this.editorRef.current) {
|
||||
this.editorRef.current.insertText(newText);
|
||||
} else {
|
||||
logger.error(`Tried to attach resource ${resource.id} to the note when the editor is not visible!`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newNote.body += `\n${resourceTag}`;
|
||||
@@ -814,31 +820,34 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 'image');
|
||||
}
|
||||
|
||||
private drawPicture_onPress = async () => {
|
||||
// Create a new empty drawing and attach it now.
|
||||
const resource = await this.attachNewDrawing('');
|
||||
await this.editDrawing(resource);
|
||||
};
|
||||
|
||||
private async updateDrawing(svgData: string) {
|
||||
let resource: ResourceEntity|null = this.state.imageEditorResource;
|
||||
|
||||
if (!resource) {
|
||||
throw new Error('No resource is loaded in the editor');
|
||||
resource = await this.attachNewDrawing(svgData);
|
||||
|
||||
// Set resouce and file path to allow
|
||||
// 1. subsequent saves to update the resource
|
||||
// 2. the editor to load from the resource's filepath (can happen
|
||||
// if the webview is reloaded).
|
||||
this.setState({
|
||||
imageEditorResourceFilepath: Resource.fullPath(resource),
|
||||
imageEditorResource: resource,
|
||||
});
|
||||
} else {
|
||||
logger.info('Saving drawing to resource', resource.id);
|
||||
|
||||
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
|
||||
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
|
||||
|
||||
resource = await Resource.updateResourceBlobContent(
|
||||
resource.id,
|
||||
tempFilePath,
|
||||
);
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
await this.refreshResource(resource);
|
||||
}
|
||||
|
||||
logger.info('Saving drawing to resource', resource.id);
|
||||
|
||||
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
|
||||
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
|
||||
|
||||
resource = await Resource.updateResourceBlobContent(
|
||||
resource.id,
|
||||
tempFilePath,
|
||||
);
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
await this.refreshResource(resource);
|
||||
}
|
||||
|
||||
private onSaveDrawing = async (svgData: string) => {
|
||||
@@ -849,6 +858,23 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ showImageEditor: false });
|
||||
};
|
||||
|
||||
private drawPicture_onPress = async () => {
|
||||
if (this.state.mode === 'edit') {
|
||||
// Create a new empty drawing and attach it now, before the image editor is opened.
|
||||
// With the present structure of Note.tsx, the we can't use this.editorRef while
|
||||
// the image editor is open, and thus can't attach drawings at the cursor locaiton.
|
||||
const resource = await this.attachNewDrawing('');
|
||||
await this.editDrawing(resource);
|
||||
} else {
|
||||
logger.info('Showing image editor...');
|
||||
this.setState({
|
||||
showImageEditor: true,
|
||||
imageEditorResourceFilepath: null,
|
||||
imageEditorResource: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private async editDrawing(item: BaseItem) {
|
||||
const filePath = Resource.fullPath(item);
|
||||
this.setState({
|
||||
@@ -1368,7 +1394,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
multiline={true}
|
||||
value={note.body}
|
||||
onChangeText={(text: string) => this.body_changeText(text)}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
onSelectionChange={this.onPlainEdtiorSelectionChange}
|
||||
blurOnSubmit={false}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
@@ -1389,7 +1415,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
initialText={note.body}
|
||||
initialSelection={this.selection}
|
||||
onChange={this.onBodyChange}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
onSelectionChange={this.onMarkdownEditorSelectionChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
onAttach={() => this.showAttachMenu()}
|
||||
readOnly={this.state.readOnly}
|
||||
@@ -1422,7 +1448,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
if (this.state.mode === 'edit') return null;
|
||||
|
||||
return <ActionButton mainButton={editButton} />;
|
||||
return <ActionButton mainButton={editButton} dispatch={this.props.dispatch} />;
|
||||
};
|
||||
|
||||
// Save button is not really needed anymore with the improved save logic
|
||||
|
@@ -16,6 +16,7 @@ const DialogBox = require('react-native-dialogbox').default;
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { BackButtonService } = require('../../services/back-button.js');
|
||||
import { AppState } from '../../utils/types';
|
||||
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids.js');
|
||||
|
||||
class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
|
||||
@@ -108,7 +109,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: any) {
|
||||
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType) {
|
||||
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType || prevProps.uncompletedTodosOnTop !== this.props.uncompletedTodosOnTop || prevProps.showCompletedTodos !== this.props.showCompletedTodos) {
|
||||
await this.refreshNotes(this.props);
|
||||
}
|
||||
}
|
||||
@@ -223,17 +224,32 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
let buttonFolderId = this.props.selectedFolderId !== Folder.conflictFolderId() ? this.props.selectedFolderId : null;
|
||||
if (!buttonFolderId) buttonFolderId = this.props.activeFolderId;
|
||||
|
||||
const addFolderNoteButtons = !!buttonFolderId;
|
||||
const isAllNotes =
|
||||
this.props.notesParentType === 'SmartFilter'
|
||||
&& this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
||||
|
||||
// Usually, when showing all notes, activeFolderId/selectedFolderId is set to the last
|
||||
// active folder.
|
||||
// If the app starts showing all notes, activeFolderId/selectedFolderId are
|
||||
// empty or null. As such, we need a special case to show the buttons:
|
||||
const addFolderNoteButtons = !!buttonFolderId || isAllNotes;
|
||||
const thisComp = this;
|
||||
|
||||
const makeActionButtonComp = () => {
|
||||
const getTargetFolderId = async () => {
|
||||
if (!buttonFolderId && isAllNotes) {
|
||||
return (await Folder.defaultFolder()).id;
|
||||
}
|
||||
return buttonFolderId;
|
||||
};
|
||||
if (addFolderNoteButtons && this.props.folders.length > 0) {
|
||||
const buttons = [];
|
||||
buttons.push({
|
||||
label: _('New to-do'),
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
const folderId = await getTargetFolderId();
|
||||
const isTodo = true;
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
void this.newNoteNavigate(folderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'checkbox-outline',
|
||||
@@ -241,14 +257,15 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
|
||||
buttons.push({
|
||||
label: _('New note'),
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
const folderId = await getTargetFolderId();
|
||||
const isTodo = false;
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
void this.newNoteNavigate(folderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'document',
|
||||
});
|
||||
return <ActionButton buttons={buttons}/>;
|
||||
return <ActionButton buttons={buttons} dispatch={this.props.dispatch}/>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@@ -523,13 +523,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 102;
|
||||
CURRENT_PROJECT_VERSION = 107;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.13.5;
|
||||
MARKETING_VERSION = 12.13.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -552,12 +552,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 102;
|
||||
CURRENT_PROJECT_VERSION = 107;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.13.5;
|
||||
MARKETING_VERSION = 12.13.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -704,14 +704,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 102;
|
||||
CURRENT_PROJECT_VERSION = 107;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.13.5;
|
||||
MARKETING_VERSION = 12.13.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -735,14 +735,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 102;
|
||||
CURRENT_PROJECT_VERSION = 107;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.13.5;
|
||||
MARKETING_VERSION = 12.13.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -359,9 +359,9 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (2.13.2):
|
||||
- react-native-saf-x (2.13.3):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.7.3):
|
||||
- react-native-safe-area-context (4.7.4):
|
||||
- React-Core
|
||||
- react-native-slider (4.4.3):
|
||||
- React-Core
|
||||
@@ -479,7 +479,8 @@ PODS:
|
||||
- React
|
||||
- RNShare (9.4.1):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.0.0):
|
||||
- RNVectorIcons (10.0.1):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
- React-Core
|
||||
- RNZipArchive (6.1.0):
|
||||
- React-Core
|
||||
@@ -786,8 +787,8 @@ SPEC CHECKSUMS:
|
||||
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
|
||||
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: a93121b21f9d5ec84d5e7fc99fdeebfbf232920a
|
||||
react-native-safe-area-context: 238cd8b619e05cb904ccad97ef42e84d1b5ae6ec
|
||||
react-native-saf-x: 0f7531c9f8bdbb62bbd55ceb7433de7bb756cd73
|
||||
react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c
|
||||
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
@@ -817,7 +818,7 @@ SPEC CHECKSUMS:
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
|
||||
RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9
|
||||
RNVectorIcons: ace237de89f1574ef3c963ae9d5da3bd6fbeb02a
|
||||
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
|
@@ -88,7 +88,7 @@
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/runtime": "7.20.0",
|
||||
"@joplin/tools": "~2.13",
|
||||
"@js-draw/material-icons": "1.11.2",
|
||||
"@js-draw/material-icons": "1.14.0",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@testing-library/jest-native": "5.4.3",
|
||||
"@testing-library/react-native": "12.3.1",
|
||||
@@ -106,7 +106,7 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jetifier": "2.0.0",
|
||||
"js-draw": "1.11.2",
|
||||
"js-draw": "1.14.0",
|
||||
"jsdom": "22.1.0",
|
||||
"metro-react-native-babel-preset": "0.73.9",
|
||||
"nodemon": "3.0.1",
|
||||
|
@@ -317,6 +317,7 @@ const appReducer = (state = appDefaultState, action: any) => {
|
||||
|
||||
if ('smartFilterId' in action) {
|
||||
newState.smartFilterId = action.smartFilterId;
|
||||
newState.selectedSmartFilterId = action.smartFilterId;
|
||||
newState.notesParentType = 'SmartFilter';
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,30 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import decoratorExtension from './decoratorExtension';
|
||||
|
||||
jest.retryTimes(2);
|
||||
|
||||
describe('decoratorExtension', () => {
|
||||
it('should highlight code blocks within tables', async () => {
|
||||
// Regression test for https://github.com/laurent22/joplin/issues/9477
|
||||
const editorText = `
|
||||
left | right
|
||||
--------|-------
|
||||
\`foo\` | bar
|
||||
`;
|
||||
const editor = await createTestEditor(
|
||||
editorText,
|
||||
|
||||
// Put the initial cursor at the start of "foo"
|
||||
EditorSelection.cursor(editorText.indexOf('foo')),
|
||||
|
||||
['TableRow', 'InlineCode'],
|
||||
[decoratorExtension],
|
||||
);
|
||||
|
||||
const codeBlock = editor.contentDOM.querySelector('.cm-inlineCode');
|
||||
|
||||
expect(codeBlock.textContent).toBe('`foo`');
|
||||
expect(codeBlock.parentElement.classList.contains('.cm-tableRow'));
|
||||
});
|
||||
});
|
@@ -72,7 +72,7 @@ const taskMarkerDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-taskMarker' },
|
||||
});
|
||||
|
||||
type DecorationDescription = { pos: number; length?: number; decoration: Decoration };
|
||||
type DecorationDescription = { pos: number; length: number; decoration: Decoration };
|
||||
|
||||
// Returns a set of [Decoration]s, associated with block syntax groups that require
|
||||
// full-line styling.
|
||||
@@ -87,6 +87,7 @@ const computeDecorations = (view: EditorView) => {
|
||||
const line = view.state.doc.lineAt(pos);
|
||||
decorations.push({
|
||||
pos: line.from,
|
||||
length: 0,
|
||||
decoration,
|
||||
});
|
||||
|
||||
@@ -185,13 +186,23 @@ const computeDecorations = (view: EditorView) => {
|
||||
});
|
||||
}
|
||||
|
||||
decorations.sort((a, b) => a.pos - b.pos);
|
||||
// Decorations need to be sorted in ascending order first by start position,
|
||||
// then by length. Adding items to the RangeSetBuilder in an incorrect order
|
||||
// causes an exception to be thrown.
|
||||
decorations.sort((a, b) => {
|
||||
const posComparison = a.pos - b.pos;
|
||||
if (posComparison !== 0) {
|
||||
return posComparison;
|
||||
}
|
||||
|
||||
const lengthComparison = a.length - b.length;
|
||||
return lengthComparison;
|
||||
});
|
||||
|
||||
// Items need to be added to a RangeSetBuilder in ascending order
|
||||
const decorationBuilder = new RangeSetBuilder<Decoration>();
|
||||
for (const { pos, length, decoration } of decorations) {
|
||||
// Null length => entire line
|
||||
decorationBuilder.add(pos, pos + (length ?? 0), decoration);
|
||||
// Zero length => entire line
|
||||
decorationBuilder.add(pos, pos + length, decoration);
|
||||
}
|
||||
return decorationBuilder.finish();
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
|
||||
import { indentUnit, syntaxTree } from '@codemirror/language';
|
||||
import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state';
|
||||
import { SelectionRange, EditorSelection, EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { MarkdownMathExtension } from '../markdown/markdownMathParser';
|
||||
import forceFullParse from './forceFullParse';
|
||||
@@ -10,7 +10,10 @@ import loadLangauges from './loadLanguages';
|
||||
// Creates and returns a minimal editor with markdown extensions. Waits to return the editor
|
||||
// until all syntax tree tags in `expectedSyntaxTreeTags` exist.
|
||||
const createTestEditor = async (
|
||||
initialText: string, initialSelection: SelectionRange, expectedSyntaxTreeTags: string[],
|
||||
initialText: string,
|
||||
initialSelection: SelectionRange,
|
||||
expectedSyntaxTreeTags: string[],
|
||||
extraExtensions: Extension[] = [],
|
||||
): Promise<EditorView> => {
|
||||
await loadLangauges();
|
||||
|
||||
@@ -23,6 +26,7 @@ const createTestEditor = async (
|
||||
}),
|
||||
indentUnit.of('\t'),
|
||||
EditorState.tabSize.of(4),
|
||||
extraExtensions,
|
||||
],
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"description": "Fast & forgiving HTML/XML/RSS parser",
|
||||
"version": "4.1.49",
|
||||
"version": "4.1.50",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/fork-sax",
|
||||
"description": "An evented streaming XML parser in JavaScript",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||
"version": "1.2.53",
|
||||
"version": "1.2.54",
|
||||
"main": "lib/sax.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-uslug",
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.15",
|
||||
"description": "A permissive slug generator that works with unicode.",
|
||||
"author": "Jeremy Selier <jerem.selier@gmail.com>",
|
||||
"publishConfig": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "2.13.2",
|
||||
"version": "2.13.3",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
@@ -34,4 +34,4 @@
|
||||
"repository": "https://github.com/laurent22/generator-joplin",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/htmlpack",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "Pack an HTML file and all its linked resources into a single HTML file",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
@@ -14,7 +14,7 @@
|
||||
"author": "Laurent Cozic",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.49",
|
||||
"@joplin/fork-htmlparser2": "^4.1.50",
|
||||
"css": "3.0.0",
|
||||
"datauri": "4.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
|
@@ -142,20 +142,28 @@ describe('import-enex-md-gen', () => {
|
||||
expect(all[0].mime).toBe('application/zip');
|
||||
});
|
||||
|
||||
it('should keep importing notes when one of them is corrupted', async () => {
|
||||
const filePath = `${enexSampleBaseDir}/ImportTestCorrupt.enex`;
|
||||
const errors: any[] = [];
|
||||
await importEnex('', filePath, {
|
||||
onError: (error: any) => errors.push(error),
|
||||
});
|
||||
const notes = await Note.all();
|
||||
expect(notes.length).toBe(2);
|
||||
// Disabled for now because the ENEX parser has become so error-tolerant
|
||||
// that it's no longer possible to generate a note that would generate a
|
||||
// failure.
|
||||
|
||||
// Check that an error was recorded and that it includes the title
|
||||
// of the note, so that it can be found back by the user
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].message.includes('Note 2')).toBe(true);
|
||||
});
|
||||
// it('should keep importing notes when one of them is corrupted', async () => {
|
||||
// const filePath = `${enexSampleBaseDir}/ImportTestCorrupt.enex`;
|
||||
// const errors: any[] = [];
|
||||
// const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(jest.fn());
|
||||
// await importEnex('', filePath, {
|
||||
// onError: (error: any) => errors.push(error),
|
||||
// });
|
||||
// consoleSpy.mockRestore();
|
||||
// const notes:NoteEntity[] = await Note.all();
|
||||
// expect(notes.length).toBe(2);
|
||||
// expect(notes.find(n => n.title === 'Note 1')).toBeTruthy();
|
||||
// expect(notes.find(n => n.title === 'Note 3')).toBeTruthy();
|
||||
|
||||
// // Check that an error was recorded and that it includes the title
|
||||
// // of the note, so that it can be found back by the user
|
||||
// expect(errors.length).toBe(1);
|
||||
// expect(errors[0].message.includes('Note 2')).toBe(true);
|
||||
// });
|
||||
|
||||
it('should throw an error and stop if the outer XML is invalid', async () => {
|
||||
await expectThrow(async () => importEnexFile('invalid_html.enex'));
|
||||
@@ -204,4 +212,12 @@ describe('import-enex-md-gen', () => {
|
||||
expect(resource.title).toBe('app_images/resizable/961b875f-24ac-402f-9b76-37e2d4f03a6c/house_500.jpg.png');
|
||||
});
|
||||
|
||||
it('should sanitize resource filenames with colons', async () => {
|
||||
await importEnexFile('resource_filename_with_colons.enex');
|
||||
const resource: ResourceEntity = (await Resource.all())[0];
|
||||
expect(resource.filename).toBe('08.06.2014165855');
|
||||
expect(resource.file_extension).toBe('2014165855');
|
||||
expect(resource.title).toBe('08.06.2014 16:58:55');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1239,6 +1239,14 @@ function drawTable(table: Section) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof tr === 'string') {
|
||||
// A <TABLE> tag should only have <TR> tags as direct children.
|
||||
// However certain Evernote notes can contain other random tags
|
||||
// such as empty DIVs. In that case we just skip the content.
|
||||
// See test "table_with_invalid_content.html".
|
||||
continue;
|
||||
}
|
||||
|
||||
const isHeader = tr.isHeader;
|
||||
const line = [];
|
||||
const headerLine = [];
|
||||
@@ -1246,6 +1254,11 @@ function drawTable(table: Section) {
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
const td = tr.lines[tdIndex];
|
||||
|
||||
if (typeof td === 'string') {
|
||||
// Same comment as above the <TR> tags.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flatRender) {
|
||||
line.push(BLOCK_OPEN);
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import shim from './shim';
|
||||
import { NoteEntity, ResourceEntity } from './services/database/types';
|
||||
import { enexXmlToMd } from './import-enex-md-gen';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import { fileExtension, friendlySafeFilename } from './path-utils';
|
||||
import { fileExtension, friendlySafeFilename, safeFileExtension } from './path-utils';
|
||||
const moment = require('moment');
|
||||
const { wrapError } = require('./errorUtils');
|
||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||
@@ -151,18 +151,22 @@ interface ExtractedNote extends NoteEntity {
|
||||
tags?: string[];
|
||||
title?: string;
|
||||
bodyXml?: string;
|
||||
// is_todo?: boolean;
|
||||
}
|
||||
|
||||
// At this point we have the resource has it's been parsed from the XML, but additional
|
||||
// processing needs to be done to get the final resource file, its size, MD5, etc.
|
||||
// At this point we have the resource as it's been parsed from the XML, but
|
||||
// additional processing needs to be done to get the final resource file, its
|
||||
// size, MD5, etc.
|
||||
async function processNoteResource(resource: ExtractedResource) {
|
||||
if (!resource.hasData) {
|
||||
// Some resources have no data, go figure, so we need a special case for this.
|
||||
resource.id = md5(Date.now() + Math.random());
|
||||
const handleNoDataResource = async (resource: ExtractedResource, setId: boolean) => {
|
||||
if (setId) resource.id = md5(Date.now() + Math.random());
|
||||
resource.size = 0;
|
||||
resource.dataFilePath = `${Setting.value('tempDir')}/${resource.id}.empty`;
|
||||
await fs.writeFile(resource.dataFilePath, '');
|
||||
};
|
||||
|
||||
if (!resource.hasData) {
|
||||
// Some resources have no data, go figure, so we need a special case for this.
|
||||
await handleNoDataResource(resource, true);
|
||||
} else {
|
||||
if (resource.dataEncoding === 'base64') {
|
||||
const decodedFilePath = `${resource.dataFilePath}.decoded`;
|
||||
@@ -176,16 +180,19 @@ async function processNoteResource(resource: ExtractedResource) {
|
||||
resource.size = stats.size;
|
||||
|
||||
if (!resource.id) {
|
||||
// If no resource ID is present, the resource ID is actually the MD5 of the data.
|
||||
// This ID will match the "hash" attribute of the corresponding <en-media> tag.
|
||||
// resourceId = md5(decodedData);
|
||||
// If no resource ID is present, the resource ID is actually the MD5
|
||||
// of the data. This ID will match the "hash" attribute of the
|
||||
// corresponding <en-media> tag. resourceId = md5(decodedData);
|
||||
resource.id = await md5File(resource.dataFilePath);
|
||||
}
|
||||
|
||||
if (!resource.id || !resource.size) {
|
||||
const debugTemp = { ...resource };
|
||||
debugTemp.data = debugTemp.data ? `${debugTemp.data.substr(0, 32)}...` : debugTemp.data;
|
||||
throw new Error(`This resource was not added because it has no ID or no content: ${JSON.stringify(debugTemp)}`);
|
||||
// Don't throw an error because it happens semi-frequently,
|
||||
// especially on notes that comes from the Evernote Web Clipper and
|
||||
// we can't do anything about it. Previously we would throw the
|
||||
// error "This resource was not added because it has no ID or no
|
||||
// content".
|
||||
await handleNoDataResource(resource, !resource.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +208,7 @@ async function saveNoteResources(note: ExtractedNote) {
|
||||
delete (toSave as any).dataFilePath;
|
||||
delete (toSave as any).dataEncoding;
|
||||
delete (toSave as any).hasData;
|
||||
toSave.file_extension = resource.filename ? fileExtension(resource.filename) : '';
|
||||
toSave.file_extension = resource.filename ? safeFileExtension(fileExtension(resource.filename)) : '';
|
||||
|
||||
// ENEX resource filenames can contain slashes, which may confuse other
|
||||
// parts of the app, which expect this `filename` field to be safe.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { defaultFolderIcon, FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
|
||||
import { defaultFolderIcon, FolderEntity, FolderIcon, NoteEntity, ResourceEntity } from '../services/database/types';
|
||||
import BaseModel, { DeleteOptions } from '../BaseModel';
|
||||
import time from '../time';
|
||||
import { _ } from '../locale';
|
||||
@@ -411,14 +411,21 @@ export default class Folder extends BaseItem {
|
||||
// resume the process from the start (thus the loop) so that we deal
|
||||
// with the right note/resource associations.
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
share_id: string;
|
||||
is_shared: number;
|
||||
resource_is_shared: number;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// Find all resources where share_id is different from parent note
|
||||
// share_id. Then update share_id on all these resources. Essentially it
|
||||
// makes it match the resource share_id to the note share_id. At the
|
||||
// same time we also process the is_shared property.
|
||||
|
||||
const rows = await this.db().selectAll(`
|
||||
SELECT r.id, n.share_id, n.is_shared
|
||||
const rows = (await this.db().selectAll(`
|
||||
SELECT r.id, n.share_id, n.is_shared, r.is_shared as resource_is_shared
|
||||
FROM note_resources nr
|
||||
LEFT JOIN resources r ON nr.resource_id = r.id
|
||||
LEFT JOIN notes n ON nr.note_id = n.id
|
||||
@@ -426,7 +433,7 @@ export default class Folder extends BaseItem {
|
||||
n.share_id != r.share_id
|
||||
OR n.is_shared != r.is_shared
|
||||
) AND nr.is_associated = 1
|
||||
`);
|
||||
`)) as Row[];
|
||||
|
||||
if (!rows.length) return;
|
||||
|
||||
@@ -434,7 +441,7 @@ export default class Folder extends BaseItem {
|
||||
|
||||
const resourceIds = rows.map(r => r.id);
|
||||
|
||||
interface Row {
|
||||
interface NoteResourceRow {
|
||||
resource_id: string;
|
||||
note_id: string;
|
||||
share_id: string;
|
||||
@@ -450,9 +457,9 @@ export default class Folder extends BaseItem {
|
||||
LEFT JOIN notes ON notes.id = note_resources.note_id
|
||||
WHERE resource_id IN ('${resourceIds.join('\',\'')}')
|
||||
AND is_associated = 1
|
||||
`) as Row[];
|
||||
`) as NoteResourceRow[];
|
||||
|
||||
const resourceIdToNotes: Record<string, Row[]> = {};
|
||||
const resourceIdToNotes: Record<string, NoteResourceRow[]> = {};
|
||||
|
||||
for (const r of noteResourceAssociations) {
|
||||
if (!resourceIdToNotes[r.resource_id]) resourceIdToNotes[r.resource_id] = [];
|
||||
@@ -496,13 +503,20 @@ export default class Folder extends BaseItem {
|
||||
} else {
|
||||
// If all is good, we can set the share_id and is_shared
|
||||
// property of the resource.
|
||||
const now = Date.now();
|
||||
for (const row of rows) {
|
||||
await Resource.save({
|
||||
const resource: ResourceEntity = {
|
||||
id: row.id,
|
||||
share_id: row.share_id || '',
|
||||
is_shared: row.is_shared,
|
||||
updated_time: Date.now(),
|
||||
}, { autoTimestamp: false });
|
||||
updated_time: now,
|
||||
};
|
||||
|
||||
if (row.is_shared !== row.resource_is_shared) {
|
||||
resource.blob_updated_time = now;
|
||||
}
|
||||
|
||||
await Resource.save(resource, { autoTimestamp: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
@@ -31,14 +31,14 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.296.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.296.0",
|
||||
"@joplin/fork-htmlparser2": "^4.1.49",
|
||||
"@joplin/fork-sax": "^1.2.53",
|
||||
"@joplin/fork-uslug": "^1.0.14",
|
||||
"@joplin/htmlpack": "^2.13.3",
|
||||
"@joplin/renderer": "^2.13.3",
|
||||
"@joplin/turndown": "^4.0.71",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.53",
|
||||
"@joplin/utils": "^2.13.3",
|
||||
"@joplin/fork-htmlparser2": "^4.1.50",
|
||||
"@joplin/fork-sax": "^1.2.54",
|
||||
"@joplin/fork-uslug": "^1.0.15",
|
||||
"@joplin/htmlpack": "^2.13.4",
|
||||
"@joplin/renderer": "^2.13.4",
|
||||
"@joplin/turndown": "^4.0.72",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.54",
|
||||
"@joplin/utils": "^2.13.4",
|
||||
"@types/nanoid": "3.0.0",
|
||||
"async-mutex": "0.4.0",
|
||||
"base-64": "1.0.0",
|
||||
|
@@ -39,6 +39,9 @@ export function isHidden(path: string) {
|
||||
return b[0] === '.';
|
||||
}
|
||||
|
||||
// Note that this function only sanitizes a file extension - it does NOT extract
|
||||
// the file extension from a filename. So the way you'd normally call this is
|
||||
// `safeFileExtension(fileExtension(filename))`
|
||||
export function safeFileExtension(e: string, maxLength: number = null) {
|
||||
// In theory the file extension can have any length but in practice Joplin
|
||||
// expects a fixed length, so we limit it to 20 which should cover most cases.
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import Note from '../../models/Note';
|
||||
import { createFolderTree, encryptionService, loadEncryptionMasterKey, msleep, resourceService, setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, supportDir, switchClient } from '../../testing/test-utils';
|
||||
import { createFolderTree, encryptionService, loadEncryptionMasterKey, msleep, resourceService, setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, supportDir, switchClient, synchronizerStart } from '../../testing/test-utils';
|
||||
import ShareService from './ShareService';
|
||||
import reducer, { defaultState } from '../../reducer';
|
||||
import { createStore } from 'redux';
|
||||
import { NoteEntity } from '../database/types';
|
||||
import { NoteEntity, ResourceEntity } from '../database/types';
|
||||
import Folder from '../../models/Folder';
|
||||
import { setEncryptionEnabled, setPpk } from '../synchronizer/syncInfoUtils';
|
||||
import { generateKeyPair } from '../e2ee/ppk';
|
||||
@@ -18,6 +18,7 @@ import BaseItem from '../../models/BaseItem';
|
||||
import ResourceService from '../ResourceService';
|
||||
import Setting from '../../models/Setting';
|
||||
import { ModelType } from '../../BaseModel';
|
||||
import { remoteNotesFoldersResources } from '../../testing/test-utils-synchronizer';
|
||||
|
||||
interface TestShareFolderServiceOptions {
|
||||
master_key_id?: string;
|
||||
@@ -36,6 +37,18 @@ function mockService(api: any) {
|
||||
return service;
|
||||
}
|
||||
|
||||
const mockServiceForNoteSharing = () => {
|
||||
return mockService({
|
||||
exec: (method: string, path = '', _query: Record<string, any> = null, _body: any = null, _headers: any = null, _options: any = null): Promise<any> => {
|
||||
if (method === 'GET' && path === 'api/shares') return { items: [] } as any;
|
||||
return null;
|
||||
},
|
||||
personalizedUserContentBaseUrl(_userId: string) {
|
||||
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('ShareService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -45,15 +58,7 @@ describe('ShareService', () => {
|
||||
|
||||
it('should not change the note user timestamps when sharing or unsharing', async () => {
|
||||
let note = await Note.save({});
|
||||
const service = mockService({
|
||||
exec: (method: string, path = '', _query: Record<string, any> = null, _body: any = null, _headers: any = null, _options: any = null): Promise<any> => {
|
||||
if (method === 'GET' && path === 'api/shares') return { items: [] } as any;
|
||||
return null;
|
||||
},
|
||||
personalizedUserContentBaseUrl(_userId: string) {
|
||||
|
||||
},
|
||||
});
|
||||
const service = mockServiceForNoteSharing();
|
||||
await msleep(1);
|
||||
await service.shareNote(note.id, false);
|
||||
|
||||
@@ -82,6 +87,46 @@ describe('ShareService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should not encrypt items that are shared', async () => {
|
||||
const folder = await Folder.save({});
|
||||
const note = await Note.save({ parent_id: folder.id });
|
||||
await shim.attachFileToNote(note, testImagePath);
|
||||
|
||||
const service = mockServiceForNoteSharing();
|
||||
|
||||
setEncryptionEnabled(true);
|
||||
await loadEncryptionMasterKey();
|
||||
|
||||
await synchronizerStart();
|
||||
|
||||
let previousBlobUpdatedTime = Infinity;
|
||||
{
|
||||
const allItems = await remoteNotesFoldersResources();
|
||||
expect(allItems.map(it => it.encryption_applied)).toEqual([1, 1, 1]);
|
||||
previousBlobUpdatedTime = allItems.find(it => it.type_ === ModelType.Resource).blob_updated_time;
|
||||
}
|
||||
|
||||
await service.shareNote(note.id, false);
|
||||
await msleep(1);
|
||||
await Folder.updateAllShareIds(resourceService());
|
||||
|
||||
await synchronizerStart();
|
||||
|
||||
{
|
||||
const allItems = await remoteNotesFoldersResources();
|
||||
expect(allItems.find(it => it.type_ === ModelType.Note).encryption_applied).toBe(0);
|
||||
expect(allItems.find(it => it.type_ === ModelType.Folder).encryption_applied).toBe(1);
|
||||
|
||||
const resource: ResourceEntity = allItems.find(it => it.type_ === ModelType.Resource);
|
||||
expect(resource.encryption_applied).toBe(0);
|
||||
|
||||
// Indicates that both the metadata and blob have been decrypted on
|
||||
// the sync target.
|
||||
expect(resource.blob_updated_time).toBe(resource.updated_time);
|
||||
expect(resource.blob_updated_time).toBeGreaterThan(previousBlobUpdatedTime);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function testShareFolderService(extraExecHandlers: Record<string, Function> = {}, options: TestShareFolderServiceOptions = {}) {
|
||||
return mockService({
|
||||
|
@@ -58,10 +58,26 @@ const shim = {
|
||||
},
|
||||
|
||||
isGNOME: () => {
|
||||
if ((!shim.isLinux() && !shim.isFreeBSD()) || !process) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentDesktop = process.env['XDG_CURRENT_DESKTOP'] ?? '';
|
||||
|
||||
// XDG_CURRENT_DESKTOP may be something like "ubuntu:GNOME" and not just "GNOME".
|
||||
// Thus, we use .includes and not ===.
|
||||
return (shim.isLinux() || shim.isFreeBSD())
|
||||
&& process && (process.env['XDG_CURRENT_DESKTOP'] ?? '').includes('GNOME');
|
||||
if (currentDesktop.includes('GNOME')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// On Ubuntu, "XDG_CURRENT_DESKTOP=ubuntu:GNOME" is replaced with "Unity" and
|
||||
// ORIGINAL_XDG_CURRENT_DESKTOP stores the original desktop.
|
||||
const originalCurrentDesktop = process.env['ORIGINAL_XDG_CURRENT_DESKTOP'] ?? '';
|
||||
if (originalCurrentDesktop.includes('GNOME')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
isFreeBSD: () => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/plugin-repo-cli",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": "./dist/index.js",
|
||||
@@ -18,9 +18,9 @@
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^2.13.3",
|
||||
"@joplin/tools": "^2.13.3",
|
||||
"@joplin/utils": "^2.13.3",
|
||||
"@joplin/lib": "^2.13.4",
|
||||
"@joplin/tools": "^2.13.4",
|
||||
"@joplin/utils": "^2.13.4",
|
||||
"fs-extra": "11.1.1",
|
||||
"gh-release-assets": "2.0.1",
|
||||
"node-fetch": "2.6.7",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/react-native-saf-x",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "a module to help work with scoped storages on android easily",
|
||||
"main": "src/index",
|
||||
"react-native": "src/index",
|
||||
|
@@ -38,6 +38,12 @@ interface RenderOptions {
|
||||
postMessageSyntax: string;
|
||||
enableLongPress: boolean;
|
||||
itemIdToUrl?: ItemIdToUrlHandler;
|
||||
allowedFilePrefixes?: string[];
|
||||
|
||||
// For compatibility with MdToHtml options:
|
||||
plugins?: {
|
||||
link_open?: { linkRenderingType?: number };
|
||||
};
|
||||
}
|
||||
|
||||
// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
|
||||
@@ -95,11 +101,13 @@ export default class HtmlToHtml {
|
||||
...options,
|
||||
};
|
||||
|
||||
const cacheKey = md5(escape(markup));
|
||||
const cacheKey = md5(escape(JSON.stringify({ markup, options })));
|
||||
let html = this.cache_.value(cacheKey);
|
||||
|
||||
if (!html) {
|
||||
html = htmlUtils.sanitizeHtml(markup);
|
||||
html = htmlUtils.sanitizeHtml(markup, {
|
||||
allowedFilePrefixes: options.allowedFilePrefixes,
|
||||
});
|
||||
|
||||
html = htmlUtils.processImageTags(html, (data: any) => {
|
||||
if (!data.src) return null;
|
||||
@@ -128,6 +136,7 @@ export default class HtmlToHtml {
|
||||
ResourceModel: this.ResourceModel_,
|
||||
postMessageSyntax: options.postMessageSyntax,
|
||||
enableLongPress: options.enableLongPress,
|
||||
...options.plugins?.link_open,
|
||||
});
|
||||
|
||||
if (!r.html) return null;
|
||||
|
@@ -192,6 +192,10 @@ export interface RuleOptions {
|
||||
vendorDir?: string;
|
||||
itemIdToUrl?: ItemIdToUrlHandler;
|
||||
|
||||
// Passed to the HTML sanitizer: Allows file:// URLs with
|
||||
// paths with the included prefixes.
|
||||
allowedFilePrefixes?: string[];
|
||||
|
||||
platformName?: string;
|
||||
}
|
||||
|
||||
|
@@ -43,12 +43,22 @@ function stringifyKatexOptions(options: any) {
|
||||
// \prob: {tokens: Array(12), numArgs: 1}
|
||||
// \expval: {tokens: Array(14), numArgs: 2}
|
||||
// \wf: {tokens: Array(6), numArgs: 0}
|
||||
// \@eqnsw: "1"
|
||||
//
|
||||
// Additionally, some KaTeX macros don't follow this general format. For example
|
||||
// \@eqnsw: "1"
|
||||
// is created by \begin{align}...\end{align} environments, and doesn't have a "tokens" property.
|
||||
|
||||
if (options.macros) {
|
||||
const toSerialize: any = {};
|
||||
for (const k of Object.keys(options.macros)) {
|
||||
const macroText: string[] = options.macros[k].tokens.map((t: any) => t.text);
|
||||
toSerialize[k] = `${macroText.join('')}_${options.macros[k].numArgs}`;
|
||||
const macro = options.macros[k];
|
||||
if (typeof macro === 'string') {
|
||||
toSerialize[k] = `${macro}_string`;
|
||||
} else {
|
||||
const macroText: string[] = macro.tokens.map((t: any) => t.text);
|
||||
toSerialize[k] = `${macroText.join('')}_${macro.numArgs}`;
|
||||
}
|
||||
}
|
||||
newOptions.macros = toSerialize;
|
||||
}
|
||||
|
@@ -34,7 +34,13 @@ export default {
|
||||
// So the sanitizeHtml function must handle this kind of non-valid HTML.
|
||||
|
||||
if (!sanitizedContent) {
|
||||
sanitizedContent = htmlUtils.sanitizeHtml(token.content, { addNoMdConvClass: true });
|
||||
sanitizedContent = htmlUtils.sanitizeHtml(
|
||||
token.content,
|
||||
{
|
||||
addNoMdConvClass: true,
|
||||
allowedFilePrefixes: ruleOptions.allowedFilePrefixes,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
token.content = sanitizedContent;
|
||||
|
@@ -189,10 +189,12 @@ class HtmlUtils {
|
||||
// If true, adds a "jop-noMdConv" class to all the tags.
|
||||
// It can be used afterwards to restore HTML tags in Markdown.
|
||||
addNoMdConvClass: false,
|
||||
allowedFilePrefixes: [],
|
||||
...options,
|
||||
};
|
||||
|
||||
// If options.allowedFilePrefixes is `undefined`, default to [].
|
||||
options.allowedFilePrefixes ??= [];
|
||||
|
||||
const output: string[] = [];
|
||||
|
||||
const tagStack: string[] = [];
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "The Joplin note renderer, used the mobile and desktop application",
|
||||
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
|
||||
"main": "index.js",
|
||||
@@ -28,9 +28,9 @@
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.49",
|
||||
"@joplin/fork-uslug": "^1.0.14",
|
||||
"@joplin/utils": "^2.13.3",
|
||||
"@joplin/fork-htmlparser2": "^4.1.50",
|
||||
"@joplin/fork-uslug": "^1.0.15",
|
||||
"@joplin/utils": "^2.13.4",
|
||||
"font-awesome-filetypes": "2.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"highlight.js": "11.8.0",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/tools",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "Various tools for Joplin",
|
||||
"main": "index.js",
|
||||
"author": "Laurent Cozic",
|
||||
@@ -20,9 +20,9 @@
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^2.13.3",
|
||||
"@joplin/renderer": "^2.13.3",
|
||||
"@joplin/utils": "^2.13.3",
|
||||
"@joplin/lib": "^2.13.4",
|
||||
"@joplin/renderer": "^2.13.4",
|
||||
"@joplin/utils": "^2.13.4",
|
||||
"compare-versions": "6.1.0",
|
||||
"dayjs": "1.11.10",
|
||||
"execa": "4.1.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/plugin-sitemap": "2.4.3",
|
||||
"@joplin/fork-htmlparser2": "^4.1.49",
|
||||
"@joplin/fork-htmlparser2": "^4.1.50",
|
||||
"@rmp135/sql-ts": "1.18.0",
|
||||
"@types/fs-extra": "11.0.3",
|
||||
"@types/jest": "29.5.5",
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "1.0.53",
|
||||
"version": "1.0.54",
|
||||
"author": "Dom Christie",
|
||||
"main": "lib/turndown-plugin-gfm.cjs.js",
|
||||
"devDependencies": {
|
||||
|
@@ -74,8 +74,8 @@ rules.tableRow = {
|
||||
rules.table = {
|
||||
// Only convert tables that can result in valid Markdown
|
||||
// Other tables are kept as HTML using `keep` (see below).
|
||||
filter: function (node) {
|
||||
return node.nodeName === 'TABLE' && !tableShouldBeHtml(node);
|
||||
filter: function (node, options) {
|
||||
return node.nodeName === 'TABLE' && !tableShouldBeHtml(node, options);
|
||||
},
|
||||
|
||||
replacement: function (content, node) {
|
||||
@@ -174,7 +174,7 @@ const nodeContains = (node, types) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tableShouldBeHtml = (tableNode, preserveNestedTables) => {
|
||||
const tableShouldBeHtml = (tableNode, options) => {
|
||||
const possibleTags = [
|
||||
'UL',
|
||||
'OL',
|
||||
@@ -193,7 +193,7 @@ const tableShouldBeHtml = (tableNode, preserveNestedTables) => {
|
||||
// that's made of HTML tables. In that case we have this logic of removing the
|
||||
// outer table and keeping only the inner ones. For the Rich Text editor
|
||||
// however we always want to keep nested tables.
|
||||
if (preserveNestedTables) possibleTags.push('TABLE');
|
||||
if (options.preserveNestedTables) possibleTags.push('TABLE');
|
||||
|
||||
return nodeContains(tableNode, 'code') ||
|
||||
nodeContains(tableNode, possibleTags);
|
||||
@@ -249,7 +249,7 @@ export default function tables (turndownService) {
|
||||
isCodeBlock_ = turndownService.isCodeBlock;
|
||||
|
||||
turndownService.keep(function (node) {
|
||||
if (node.nodeName === 'TABLE' && tableShouldBeHtml(node, turndownService.options.preserveNestedTables)) return true;
|
||||
if (node.nodeName === 'TABLE' && tableShouldBeHtml(node, turndownService.options)) return true;
|
||||
return false;
|
||||
});
|
||||
for (var key in rules) turndownService.addRule(key, rules[key])
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/turndown",
|
||||
"description": "A library that converts HTML to Markdown",
|
||||
"version": "4.0.71",
|
||||
"version": "4.0.72",
|
||||
"author": "Dom Christie",
|
||||
"main": "lib/turndown.cjs.js",
|
||||
"publishConfig": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/utils",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.4",
|
||||
"description": "Utilities for Joplin",
|
||||
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/utils",
|
||||
"exports": {
|
||||
|
@@ -1,5 +1,20 @@
|
||||
# Joplin Android Changelog
|
||||
|
||||
## [android-v2.13.10](https://github.com/laurent22/joplin/releases/tag/android-v2.13.10) (Pre-release) - 2023-12-01T11:16:17Z
|
||||
|
||||
- Improved: Drawing: Revert recent changes to input system (#9426) (#9427 by Henry Heino)
|
||||
|
||||
## [android-v2.13.9](https://github.com/laurent22/joplin/releases/tag/android-v2.13.9) (Pre-release) - 2023-11-30T17:55:54Z
|
||||
|
||||
- Improved: Don't attach empty drawings when a user exits without saving (#9386) (#9377 by Henry Heino)
|
||||
- Fixed: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) (#9374 by Henry Heino)
|
||||
|
||||
## [android-v2.13.8](https://github.com/laurent22/joplin/releases/tag/android-v2.13.8) (Pre-release) - 2023-11-26T12:37:00Z
|
||||
|
||||
- Fixed: Fix to-dos options toggle don't toggle a rerender (#9364) (#9361 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Fix new note/to-do buttons not visible on app startup in some cases (#9329) (#9328 by Henry Heino)
|
||||
- Fixed: Sidebar is not dismissed when creating a note (#9376)
|
||||
|
||||
## [android-v2.13.7](https://github.com/laurent22/joplin/releases/tag/android-v2.13.7) (Pre-release) - 2023-11-16T13:17:53Z
|
||||
|
||||
- Improved: Add more space between settings title and description (#9270) (#9258 by Henry Heino)
|
||||
|
@@ -1,5 +1,11 @@
|
||||
# Joplin Terminal App Changelog
|
||||
|
||||
## [cli-v2.13.2](https://github.com/laurent22/joplin/releases/tag/cli-v2.13.2) - 2023-11-30T18:11:38Z
|
||||
|
||||
- Improved: Updated packages mermaid (v10.5.1), sass (v1.69.5)
|
||||
- Fixed: Import of inter-linked md files has incorrect notebook structure (#9269) (#9151 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Work around WebDAV sync issues over ipv6 (#9286) (#8788 by Henry Heino)
|
||||
|
||||
## [cli-v2.13.1](https://github.com/laurent22/joplin/releases/tag/cli-v2.13.1) - 2023-11-09T20:08:17Z
|
||||
|
||||
- Improved: Allow modifying a resource metadata only when synchronising (#9114)
|
||||
|
@@ -1,5 +1,35 @@
|
||||
# Joplin iOS Changelog
|
||||
|
||||
## [ios-v12.13.10](https://github.com/laurent22/joplin/releases/tag/ios-v12.13.10) - 2023-12-01T12:07:57Z
|
||||
|
||||
- Improved: Drawing: Revert recent changes to input system (#9426) (#9427 by Henry Heino)
|
||||
|
||||
## [ios-v12.13.9](https://github.com/laurent22/joplin/releases/tag/ios-v12.13.9) - 2023-11-30T17:56:37Z
|
||||
|
||||
- Improved: Don't attach empty drawings when a user exits without saving (#9386) (#9377 by Henry Heino)
|
||||
- Fixed: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) (#9374 by Henry Heino)
|
||||
|
||||
## [ios-v12.13.8](https://github.com/laurent22/joplin/releases/tag/ios-v12.13.8) - 2023-11-26T12:54:44Z
|
||||
|
||||
- Fixed: Fix to-dos options toggle don't toggle a rerender (#9364) (#9361 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Fix new note/to-do buttons not visible on app startup in some cases (#9329) (#9328 by Henry Heino)
|
||||
- Fixed: Sidebar is not dismissed when creating a note (#9376)
|
||||
|
||||
## [ios-v12.13.7](https://github.com/laurent22/joplin/releases/tag/ios-v12.13.7) - 2023-11-16T13:37:03Z
|
||||
|
||||
- Improved: Add more space between settings title and description (#9270) (#9258 by Henry Heino)
|
||||
- Improved: Fade settings screen icons (#9268) (#9260 by Henry Heino)
|
||||
- Improved: Implement settings search (#9320) (#9294 by Henry Heino)
|
||||
- Improved: Improve image editor load performance (#9281 by Henry Heino)
|
||||
- Improved: Update js-draw to version 1.11.2 (#9120) (#9195 by Henry Heino)
|
||||
- Improved: Updated packages @testing-library/react-native (v12.3.1), mermaid (v10.5.1), react-native-safe-area-context (v4.7.4), react-native-vector-icons (v10.0.1), sass (v1.69.5)
|
||||
- Fixed: Allow showing dropdowns in landscape mode (#9309) (#9271 by Henry Heino)
|
||||
- Fixed: Config screen: Fix section list scroll (#9267) (#9259 by Henry Heino)
|
||||
- Fixed: Disable notebook list side menu in config screen (#9311) (#9308 by Henry Heino)
|
||||
- Fixed: Fix encryption when a resource doesn't have an associated file (#9222) (#9123 by Henry Heino)
|
||||
- Fixed: Fix settings save confirmation not shown when navigating to encryption/profile/log screens (#9313) (#9312 by Henry Heino)
|
||||
- Fixed: Restore scroll position when returning to the note viewer from the editor or camera (#9324) (#9321 by Henry Heino)
|
||||
|
||||
## [ios-v12.13.5](https://github.com/laurent22/joplin/releases/tag/ios-v12.13.5) - 2023-11-10T13:20:09Z
|
||||
|
||||
- Improved: Add a "Retry all" button when multiple resources could not be downloaded (#9158)
|
||||
|
78
yarn.lock
78
yarn.lock
@@ -6293,7 +6293,7 @@ __metadata:
|
||||
"@joplin/renderer": ~2.13
|
||||
"@joplin/tools": ~2.13
|
||||
"@joplin/utils": ~2.13
|
||||
"@js-draw/material-icons": 1.11.2
|
||||
"@js-draw/material-icons": 1.14.0
|
||||
"@lezer/highlight": 1.1.4
|
||||
"@react-native-community/clipboard": 1.5.1
|
||||
"@react-native-community/datetimepicker": 7.6.1
|
||||
@@ -6323,7 +6323,7 @@ __metadata:
|
||||
jest: 29.7.0
|
||||
jest-environment-jsdom: 29.7.0
|
||||
jetifier: 2.0.0
|
||||
js-draw: 1.11.2
|
||||
js-draw: 1.14.0
|
||||
jsc-android: 241213.1.0
|
||||
jsdom: 22.1.0
|
||||
lodash: 4.17.21
|
||||
@@ -6437,7 +6437,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/fork-htmlparser2@^4.1.49, @joplin/fork-htmlparser2@workspace:packages/fork-htmlparser2":
|
||||
"@joplin/fork-htmlparser2@^4.1.50, @joplin/fork-htmlparser2@workspace:packages/fork-htmlparser2":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/fork-htmlparser2@workspace:packages/fork-htmlparser2"
|
||||
dependencies:
|
||||
@@ -6458,7 +6458,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/fork-sax@^1.2.53, @joplin/fork-sax@workspace:packages/fork-sax":
|
||||
"@joplin/fork-sax@^1.2.54, @joplin/fork-sax@workspace:packages/fork-sax":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/fork-sax@workspace:packages/fork-sax"
|
||||
dependencies:
|
||||
@@ -6467,7 +6467,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/fork-uslug@^1.0.14, @joplin/fork-uslug@workspace:packages/fork-uslug":
|
||||
"@joplin/fork-uslug@^1.0.15, @joplin/fork-uslug@workspace:packages/fork-uslug":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/fork-uslug@workspace:packages/fork-uslug"
|
||||
dependencies:
|
||||
@@ -6477,11 +6477,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/htmlpack@^2.13.3, @joplin/htmlpack@workspace:packages/htmlpack":
|
||||
"@joplin/htmlpack@^2.13.4, @joplin/htmlpack@workspace:packages/htmlpack":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/htmlpack@workspace:packages/htmlpack"
|
||||
dependencies:
|
||||
"@joplin/fork-htmlparser2": ^4.1.49
|
||||
"@joplin/fork-htmlparser2": ^4.1.50
|
||||
"@types/fs-extra": 11.0.3
|
||||
css: 3.0.0
|
||||
datauri: 4.1.0
|
||||
@@ -6490,20 +6490,20 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/lib@^2.13.3, @joplin/lib@workspace:packages/lib, @joplin/lib@~2.13":
|
||||
"@joplin/lib@^2.13.4, @joplin/lib@workspace:packages/lib, @joplin/lib@~2.13":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/lib@workspace:packages/lib"
|
||||
dependencies:
|
||||
"@aws-sdk/client-s3": 3.296.0
|
||||
"@aws-sdk/s3-request-presigner": 3.296.0
|
||||
"@joplin/fork-htmlparser2": ^4.1.49
|
||||
"@joplin/fork-sax": ^1.2.53
|
||||
"@joplin/fork-uslug": ^1.0.14
|
||||
"@joplin/htmlpack": ^2.13.3
|
||||
"@joplin/renderer": ^2.13.3
|
||||
"@joplin/turndown": ^4.0.71
|
||||
"@joplin/turndown-plugin-gfm": ^1.0.53
|
||||
"@joplin/utils": ^2.13.3
|
||||
"@joplin/fork-htmlparser2": ^4.1.50
|
||||
"@joplin/fork-sax": ^1.2.54
|
||||
"@joplin/fork-uslug": ^1.0.15
|
||||
"@joplin/htmlpack": ^2.13.4
|
||||
"@joplin/renderer": ^2.13.4
|
||||
"@joplin/turndown": ^4.0.72
|
||||
"@joplin/turndown-plugin-gfm": ^1.0.54
|
||||
"@joplin/utils": ^2.13.4
|
||||
"@types/fs-extra": 11.0.3
|
||||
"@types/jest": 29.5.5
|
||||
"@types/js-yaml": 4.0.8
|
||||
@@ -6607,9 +6607,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/plugin-repo-cli@workspace:packages/plugin-repo-cli"
|
||||
dependencies:
|
||||
"@joplin/lib": ^2.13.3
|
||||
"@joplin/tools": ^2.13.3
|
||||
"@joplin/utils": ^2.13.3
|
||||
"@joplin/lib": ^2.13.4
|
||||
"@joplin/tools": ^2.13.4
|
||||
"@joplin/utils": ^2.13.4
|
||||
"@types/fs-extra": 11.0.3
|
||||
"@types/jest": 29.5.5
|
||||
"@types/node": 18.18.7
|
||||
@@ -6658,13 +6658,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/renderer@^2.13.3, @joplin/renderer@workspace:packages/renderer, @joplin/renderer@~2.13":
|
||||
"@joplin/renderer@^2.13.4, @joplin/renderer@workspace:packages/renderer, @joplin/renderer@~2.13":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/renderer@workspace:packages/renderer"
|
||||
dependencies:
|
||||
"@joplin/fork-htmlparser2": ^4.1.49
|
||||
"@joplin/fork-uslug": ^1.0.14
|
||||
"@joplin/utils": ^2.13.3
|
||||
"@joplin/fork-htmlparser2": ^4.1.50
|
||||
"@joplin/fork-uslug": ^1.0.15
|
||||
"@joplin/utils": ^2.13.4
|
||||
"@types/jest": 29.5.5
|
||||
"@types/markdown-it": 13.0.5
|
||||
"@types/node": 18.18.7
|
||||
@@ -6761,15 +6761,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/tools@^2.13.3, @joplin/tools@workspace:packages/tools, @joplin/tools@~2.13":
|
||||
"@joplin/tools@^2.13.4, @joplin/tools@workspace:packages/tools, @joplin/tools@~2.13":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/tools@workspace:packages/tools"
|
||||
dependencies:
|
||||
"@docusaurus/plugin-sitemap": 2.4.3
|
||||
"@joplin/fork-htmlparser2": ^4.1.49
|
||||
"@joplin/lib": ^2.13.3
|
||||
"@joplin/renderer": ^2.13.3
|
||||
"@joplin/utils": ^2.13.3
|
||||
"@joplin/fork-htmlparser2": ^4.1.50
|
||||
"@joplin/lib": ^2.13.4
|
||||
"@joplin/renderer": ^2.13.4
|
||||
"@joplin/utils": ^2.13.4
|
||||
"@rmp135/sql-ts": 1.18.0
|
||||
"@types/fs-extra": 11.0.3
|
||||
"@types/jest": 29.5.5
|
||||
@@ -6809,7 +6809,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/turndown-plugin-gfm@^1.0.53, @joplin/turndown-plugin-gfm@workspace:packages/turndown-plugin-gfm":
|
||||
"@joplin/turndown-plugin-gfm@^1.0.54, @joplin/turndown-plugin-gfm@workspace:packages/turndown-plugin-gfm":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/turndown-plugin-gfm@workspace:packages/turndown-plugin-gfm"
|
||||
dependencies:
|
||||
@@ -6821,7 +6821,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/turndown@^4.0.71, @joplin/turndown@workspace:packages/turndown":
|
||||
"@joplin/turndown@^4.0.72, @joplin/turndown@workspace:packages/turndown":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/turndown@workspace:packages/turndown"
|
||||
dependencies:
|
||||
@@ -6838,7 +6838,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@joplin/utils@^2.13.3, @joplin/utils@workspace:packages/utils, @joplin/utils@~2.13":
|
||||
"@joplin/utils@^2.13.4, @joplin/utils@workspace:packages/utils, @joplin/utils@~2.13":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@joplin/utils@workspace:packages/utils"
|
||||
dependencies:
|
||||
@@ -6990,12 +6990,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@js-draw/material-icons@npm:1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "@js-draw/material-icons@npm:1.11.2"
|
||||
"@js-draw/material-icons@npm:1.14.0":
|
||||
version: 1.14.0
|
||||
resolution: "@js-draw/material-icons@npm:1.14.0"
|
||||
peerDependencies:
|
||||
js-draw: ^1.0.1
|
||||
checksum: 6a6bbdf936d3a97fab43321d807672f157b12201290a98bd3fd33a7e53966647ed9c5a8aba5dfd6d743bfc37ab9ddff14cbb7fc3f4ffb8b79ff7617a7e886160
|
||||
checksum: 6e67ee6399b9b4f9e5891952e71a978acd69c0d386fe80c957ad884ab8e9f4f954aa0d9dd1b08e78f314b0b8f3807989a482ed4a153457a8bba67f5989dd7a0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -26295,13 +26295,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-draw@npm:1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "js-draw@npm:1.11.2"
|
||||
"js-draw@npm:1.14.0":
|
||||
version: 1.14.0
|
||||
resolution: "js-draw@npm:1.14.0"
|
||||
dependencies:
|
||||
"@js-draw/math": ^1.11.1
|
||||
"@melloware/coloris": 0.22.0
|
||||
checksum: 59669bbe37f4c980f8532b96ec7a80880966beca3a82973fa0681c3988d8ed12745d5c1b27645853806087664c14fa49530fc8815a471346f03b3ef7c4366ea9
|
||||
checksum: 0e2bbf318a8ebc645ed83f8cf0ef1f43a49d85b5f83e0be1616ec200d37817070a002bda54ef21df72d74a37597aa61e61e94559b27fbe4de8fcdb70effa04d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user