You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-08 23:07:32 +02:00
Compare commits
10 Commits
server-v3.
...
plugin-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15394a3f7e | ||
|
|
c3cdeeb315 | ||
|
|
af882612e9 | ||
|
|
11fffc0e4e | ||
|
|
ea2d9ca467 | ||
|
|
e027cdce26 | ||
|
|
229b9a580e | ||
|
|
a6df9f119c | ||
|
|
6670f9ae1c | ||
|
|
09506a7b40 |
@@ -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/activateMainMenuItem.js
|
||||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.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/integration-tests/util/test.js
|
||||||
packages/app-desktop/playwright.config.js
|
packages/app-desktop/playwright.config.js
|
||||||
packages/app-desktop/plugins/GotoAnything.js
|
packages/app-desktop/plugins/GotoAnything.js
|
||||||
|
|||||||
1
.gitignore
vendored
1
.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/activateMainMenuItem.js
|
||||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.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/integration-tests/util/test.js
|
||||||
packages/app-desktop/playwright.config.js
|
packages/app-desktop/playwright.config.js
|
||||||
packages/app-desktop/plugins/GotoAnything.js
|
packages/app-desktop/plugins/GotoAnything.js
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
|||||||
<table>
|
<table>
|
||||||
|
<div></div> <!-- INVALID! -->
|
||||||
<tr>
|
<tr>
|
||||||
<td>one</td>
|
<td>one</td>
|
||||||
<td>two</td>
|
<td>two</td>
|
||||||
|
|||||||
@@ -856,7 +856,18 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
const resourcesEqual = resourceInfosEqual(lastOnChangeEventInfo.current.resourceInfos, props.resourceInfos);
|
const resourcesEqual = resourceInfosEqual(lastOnChangeEventInfo.current.resourceInfos, props.resourceInfos);
|
||||||
|
|
||||||
if (lastOnChangeEventInfo.current.content !== props.content || !resourcesEqual) {
|
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;
|
if (cancelled) return;
|
||||||
|
|
||||||
editor.setContent(awfulBrHack(result.html));
|
editor.setContent(awfulBrHack(result.html));
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ function NoteEditor(props: NoteEditorProps) {
|
|||||||
contentMarkupLanguage: formNote.markup_language,
|
contentMarkupLanguage: formNote.markup_language,
|
||||||
contentOriginalCss: formNote.originalCss,
|
contentOriginalCss: formNote.originalCss,
|
||||||
resourceInfos: resourceInfos,
|
resourceInfos: resourceInfos,
|
||||||
|
resourceDirectory: Setting.value('resourceDir'),
|
||||||
htmlToMarkdown: htmlToMarkdown,
|
htmlToMarkdown: htmlToMarkdown,
|
||||||
markupToHtml: markupToHtml,
|
markupToHtml: markupToHtml,
|
||||||
allAssets: allAssets,
|
allAssets: allAssets,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export interface NoteBodyEditorProps {
|
|||||||
visiblePanes: string[];
|
visiblePanes: string[];
|
||||||
keyboardMode: string;
|
keyboardMode: string;
|
||||||
resourceInfos: ResourceInfos;
|
resourceInfos: ResourceInfos;
|
||||||
|
resourceDirectory: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
onDrop: Function;
|
onDrop: Function;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface MarkupToHtmlOptions {
|
|||||||
noteId?: string;
|
noteId?: string;
|
||||||
vendorDir?: string;
|
vendorDir?: string;
|
||||||
platformName?: string;
|
platformName?: string;
|
||||||
|
allowedFilePrefixes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useMarkupToHtml(deps: HookDependencies) {
|
export default function useMarkupToHtml(deps: HookDependencies) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { writeFile } from 'fs-extra';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import createStartupArgs from './util/createStartupArgs';
|
import createStartupArgs from './util/createStartupArgs';
|
||||||
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
|
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
|
||||||
|
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||||
|
|
||||||
|
|
||||||
test.describe('main', () => {
|
test.describe('main', () => {
|
||||||
@@ -20,17 +21,7 @@ test.describe('main', () => {
|
|||||||
|
|
||||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||||
const mainScreen = new MainScreen(mainWindow);
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
await mainScreen.newNoteButton.click();
|
const editor = await mainScreen.createNewNote('Test note');
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
// Note list should contain the new note
|
// Note list should contain the new note
|
||||||
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
|
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
|
||||||
@@ -49,6 +40,50 @@ test.describe('main', () => {
|
|||||||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
|
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 }) => {
|
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
||||||
const mainScreen = new MainScreen(mainWindow);
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
await mainScreen.waitFor();
|
await mainScreen.waitFor();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default class MainScreen {
|
|||||||
public readonly noteListContainer: Locator;
|
public readonly noteListContainer: Locator;
|
||||||
public readonly noteEditor: NoteEditorScreen;
|
public readonly noteEditor: NoteEditorScreen;
|
||||||
|
|
||||||
public constructor(page: Page) {
|
public constructor(private page: Page) {
|
||||||
this.newNoteButton = page.locator('.new-note-button');
|
this.newNoteButton = page.locator('.new-note-button');
|
||||||
this.noteListContainer = page.locator('.rli-noteList');
|
this.noteListContainer = page.locator('.rli-noteList');
|
||||||
this.noteEditor = new NoteEditorScreen(page);
|
this.noteEditor = new NoteEditorScreen(page);
|
||||||
@@ -17,4 +17,20 @@ export default class MainScreen {
|
|||||||
await this.noteEditor.waitFor();
|
await this.noteEditor.waitFor();
|
||||||
await this.noteListContainer.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 {
|
export default class NoteEditorPage {
|
||||||
public readonly codeMirrorEditor: Locator;
|
public readonly codeMirrorEditor: Locator;
|
||||||
|
public readonly richTextEditor: Locator;
|
||||||
public readonly noteTitleInput: Locator;
|
public readonly noteTitleInput: Locator;
|
||||||
|
public readonly attachFileButton: Locator;
|
||||||
|
public readonly toggleEditorsButton: Locator;
|
||||||
private readonly containerLocator: Locator;
|
private readonly containerLocator: Locator;
|
||||||
|
|
||||||
public constructor(private readonly page: Page) {
|
public constructor(private readonly page: Page) {
|
||||||
this.containerLocator = page.locator('.rli-editor');
|
this.containerLocator = page.locator('.rli-editor');
|
||||||
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
|
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
|
||||||
|
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
||||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
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() {
|
public getNoteViewerIframe() {
|
||||||
@@ -19,8 +25,15 @@ export default class NoteEditorPage {
|
|||||||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
|
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() {
|
public async waitFor() {
|
||||||
await this.codeMirrorEditor.waitFor();
|
|
||||||
await this.noteTitleInput.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",
|
"name": "@joplin/app-desktop",
|
||||||
"version": "2.13.10",
|
"version": "2.13.13",
|
||||||
"description": "Joplin for Desktop",
|
"description": "Joplin for Desktop",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "generator-joplin",
|
"name": "generator-joplin",
|
||||||
"version": "2.13.2",
|
"version": "2.13.3",
|
||||||
"description": "Scaffolds out a new Joplin plugin",
|
"description": "Scaffolds out a new Joplin plugin",
|
||||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -34,4 +34,4 @@
|
|||||||
"repository": "https://github.com/laurent22/generator-joplin",
|
"repository": "https://github.com/laurent22/generator-joplin",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
@@ -142,20 +142,28 @@ describe('import-enex-md-gen', () => {
|
|||||||
expect(all[0].mime).toBe('application/zip');
|
expect(all[0].mime).toBe('application/zip');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep importing notes when one of them is corrupted', async () => {
|
// Disabled for now because the ENEX parser has become so error-tolerant
|
||||||
const filePath = `${enexSampleBaseDir}/ImportTestCorrupt.enex`;
|
// that it's no longer possible to generate a note that would generate a
|
||||||
const errors: any[] = [];
|
// failure.
|
||||||
await importEnex('', filePath, {
|
|
||||||
onError: (error: any) => errors.push(error),
|
|
||||||
});
|
|
||||||
const notes = await Note.all();
|
|
||||||
expect(notes.length).toBe(2);
|
|
||||||
|
|
||||||
// Check that an error was recorded and that it includes the title
|
// it('should keep importing notes when one of them is corrupted', async () => {
|
||||||
// of the note, so that it can be found back by the user
|
// const filePath = `${enexSampleBaseDir}/ImportTestCorrupt.enex`;
|
||||||
expect(errors.length).toBe(1);
|
// const errors: any[] = [];
|
||||||
expect(errors[0].message.includes('Note 2')).toBe(true);
|
// 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 () => {
|
it('should throw an error and stop if the outer XML is invalid', async () => {
|
||||||
await expectThrow(async () => importEnexFile('invalid_html.enex'));
|
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');
|
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;
|
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 isHeader = tr.isHeader;
|
||||||
const line = [];
|
const line = [];
|
||||||
const headerLine = [];
|
const headerLine = [];
|
||||||
@@ -1247,10 +1255,7 @@ function drawTable(table: Section) {
|
|||||||
const td = tr.lines[tdIndex];
|
const td = tr.lines[tdIndex];
|
||||||
|
|
||||||
if (typeof td === 'string') {
|
if (typeof td === 'string') {
|
||||||
// A <TR> tag should only have <TD> tags as direct children.
|
// Same comment as above the <TR> tags.
|
||||||
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import shim from './shim';
|
|||||||
import { NoteEntity, ResourceEntity } from './services/database/types';
|
import { NoteEntity, ResourceEntity } from './services/database/types';
|
||||||
import { enexXmlToMd } from './import-enex-md-gen';
|
import { enexXmlToMd } from './import-enex-md-gen';
|
||||||
import { MarkupToHtml } from '@joplin/renderer';
|
import { MarkupToHtml } from '@joplin/renderer';
|
||||||
import { fileExtension, friendlySafeFilename } from './path-utils';
|
import { fileExtension, friendlySafeFilename, safeFileExtension } from './path-utils';
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const { wrapError } = require('./errorUtils');
|
const { wrapError } = require('./errorUtils');
|
||||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||||
@@ -208,7 +208,7 @@ async function saveNoteResources(note: ExtractedNote) {
|
|||||||
delete (toSave as any).dataFilePath;
|
delete (toSave as any).dataFilePath;
|
||||||
delete (toSave as any).dataEncoding;
|
delete (toSave as any).dataEncoding;
|
||||||
delete (toSave as any).hasData;
|
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
|
// ENEX resource filenames can contain slashes, which may confuse other
|
||||||
// parts of the app, which expect this `filename` field to be safe.
|
// parts of the app, which expect this `filename` field to be safe.
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export function isHidden(path: string) {
|
|||||||
return b[0] === '.';
|
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) {
|
export function safeFileExtension(e: string, maxLength: number = null) {
|
||||||
// In theory the file extension can have any length but in practice Joplin
|
// 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.
|
// expects a fixed length, so we limit it to 20 which should cover most cases.
|
||||||
|
|||||||
@@ -58,10 +58,26 @@ const shim = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isGNOME: () => {
|
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".
|
// XDG_CURRENT_DESKTOP may be something like "ubuntu:GNOME" and not just "GNOME".
|
||||||
// Thus, we use .includes and not ===.
|
// Thus, we use .includes and not ===.
|
||||||
return (shim.isLinux() || shim.isFreeBSD())
|
if (currentDesktop.includes('GNOME')) {
|
||||||
&& process && (process.env['XDG_CURRENT_DESKTOP'] ?? '').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: () => {
|
isFreeBSD: () => {
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ interface RenderOptions {
|
|||||||
postMessageSyntax: string;
|
postMessageSyntax: string;
|
||||||
enableLongPress: boolean;
|
enableLongPress: boolean;
|
||||||
itemIdToUrl?: ItemIdToUrlHandler;
|
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
|
// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
|
||||||
@@ -95,11 +101,13 @@ export default class HtmlToHtml {
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheKey = md5(escape(markup));
|
const cacheKey = md5(escape(JSON.stringify({ markup, options })));
|
||||||
let html = this.cache_.value(cacheKey);
|
let html = this.cache_.value(cacheKey);
|
||||||
|
|
||||||
if (!html) {
|
if (!html) {
|
||||||
html = htmlUtils.sanitizeHtml(markup);
|
html = htmlUtils.sanitizeHtml(markup, {
|
||||||
|
allowedFilePrefixes: options.allowedFilePrefixes,
|
||||||
|
});
|
||||||
|
|
||||||
html = htmlUtils.processImageTags(html, (data: any) => {
|
html = htmlUtils.processImageTags(html, (data: any) => {
|
||||||
if (!data.src) return null;
|
if (!data.src) return null;
|
||||||
@@ -128,6 +136,7 @@ export default class HtmlToHtml {
|
|||||||
ResourceModel: this.ResourceModel_,
|
ResourceModel: this.ResourceModel_,
|
||||||
postMessageSyntax: options.postMessageSyntax,
|
postMessageSyntax: options.postMessageSyntax,
|
||||||
enableLongPress: options.enableLongPress,
|
enableLongPress: options.enableLongPress,
|
||||||
|
...options.plugins?.link_open,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!r.html) return null;
|
if (!r.html) return null;
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ export interface RuleOptions {
|
|||||||
vendorDir?: string;
|
vendorDir?: string;
|
||||||
itemIdToUrl?: ItemIdToUrlHandler;
|
itemIdToUrl?: ItemIdToUrlHandler;
|
||||||
|
|
||||||
|
// Passed to the HTML sanitizer: Allows file:// URLs with
|
||||||
|
// paths with the included prefixes.
|
||||||
|
allowedFilePrefixes?: string[];
|
||||||
|
|
||||||
platformName?: string;
|
platformName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ export default {
|
|||||||
// So the sanitizeHtml function must handle this kind of non-valid HTML.
|
// So the sanitizeHtml function must handle this kind of non-valid HTML.
|
||||||
|
|
||||||
if (!sanitizedContent) {
|
if (!sanitizedContent) {
|
||||||
sanitizedContent = htmlUtils.sanitizeHtml(token.content, { addNoMdConvClass: true });
|
sanitizedContent = htmlUtils.sanitizeHtml(
|
||||||
|
token.content,
|
||||||
|
{
|
||||||
|
addNoMdConvClass: true,
|
||||||
|
allowedFilePrefixes: ruleOptions.allowedFilePrefixes,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
token.content = sanitizedContent;
|
token.content = sanitizedContent;
|
||||||
|
|||||||
@@ -189,10 +189,12 @@ class HtmlUtils {
|
|||||||
// If true, adds a "jop-noMdConv" class to all the tags.
|
// If true, adds a "jop-noMdConv" class to all the tags.
|
||||||
// It can be used afterwards to restore HTML tags in Markdown.
|
// It can be used afterwards to restore HTML tags in Markdown.
|
||||||
addNoMdConvClass: false,
|
addNoMdConvClass: false,
|
||||||
allowedFilePrefixes: [],
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If options.allowedFilePrefixes is `undefined`, default to [].
|
||||||
|
options.allowedFilePrefixes ??= [];
|
||||||
|
|
||||||
const output: string[] = [];
|
const output: string[] = [];
|
||||||
|
|
||||||
const tagStack: string[] = [];
|
const tagStack: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user