You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-03-12 10:00:05 +02:00
Compare commits
33 Commits
v3.6.4
...
fix_contex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68d1601847 | ||
|
|
2132c2cdf4 | ||
|
|
67aff20e39 | ||
|
|
3719e1eee0 | ||
|
|
4abe83fdb6 | ||
|
|
6ba912e5aa | ||
|
|
8533083730 | ||
|
|
754ff28b36 | ||
|
|
b663c64def | ||
|
|
998b26d9a4 | ||
|
|
b097cf9a6a | ||
|
|
e22c367566 | ||
|
|
71a2e98155 | ||
|
|
714bbd6d23 | ||
|
|
eda03333a6 | ||
|
|
93f17a87fa | ||
|
|
c765306e6f | ||
|
|
f05fe5754d | ||
|
|
d046bfa14b | ||
|
|
2a681008dd | ||
|
|
7214823c74 | ||
|
|
ed5b92a91e | ||
|
|
2c8a9eee61 | ||
|
|
6451305c89 | ||
|
|
5fd0dc23da | ||
|
|
fd3b133b16 | ||
|
|
118bc3edf1 | ||
|
|
d90836bc50 | ||
|
|
9a477dbeb9 | ||
|
|
5271081b3a | ||
|
|
b26370fc5a | ||
|
|
737c7dcdb4 | ||
|
|
04babe0261 |
@@ -271,6 +271,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useEditorSettings.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
|
||||
2
.github/workflows/build-android.yml
vendored
2
.github/workflows/build-android.yml
vendored
@@ -5,7 +5,7 @@ name: react-native-android-build-apk
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/build-macos-m1.yml
vendored
2
.github/workflows/build-macos-m1.yml
vendored
@@ -2,7 +2,7 @@ name: Build macOS M1
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/github-actions-main.yml
vendored
2
.github/workflows/github-actions-main.yml
vendored
@@ -2,7 +2,7 @@ name: Joplin Continuous Integration
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@@ -2,7 +2,7 @@ name: Joplin UI tests
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -244,6 +244,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useEditorSettings.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
|
||||
@@ -1351,11 +1351,7 @@ footer .bottom-links-row p {
|
||||
ENGLISH VERSION
|
||||
*****************************************************************/
|
||||
|
||||
:lang(en-gb) #made-in-france-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:lang(en-gb) .top-section-img-cn {
|
||||
:not(:lang(zh-cn)) .top-section-img-cn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ function setupLocaleRedirect() {
|
||||
if (!isRootPage) return;
|
||||
|
||||
// Check if user has explicitly chosen to stay on current locale
|
||||
const localePreference = localStorage.getItem('joplin-locale-preference');
|
||||
const localePreference = (localStorage.getItem('joplin-locale-preference') || '').toLowerCase();
|
||||
if (localePreference === 'en') return;
|
||||
|
||||
// Get user's preferred language from browser
|
||||
@@ -160,9 +160,10 @@ function setupLocaleRedirect() {
|
||||
window.location.href = getLocalePath(langCode) + '/';
|
||||
}
|
||||
|
||||
// Allow users to switch back to English and remember their preference
|
||||
function setLocalePreference(locale) {
|
||||
// Allow users to switch language and remember their preference
|
||||
function setLocalePreference(locale, url) {
|
||||
localStorage.setItem('joplin-locale-preference', locale);
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Expose globally for language switcher links
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<div class="col-9 text-right d-none d-md-block">
|
||||
{{> twitterLink}}
|
||||
<a href="{{baseUrl}}/news/" class="fw500">News</a>
|
||||
<a href="{{baseUrl}}/plugins/" class="fw500">Plugins</a>
|
||||
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500">Forum</a>
|
||||
|
||||
@@ -23,7 +24,7 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{{#availableLocales}}
|
||||
<li><a class="dropdown-item {{#isActive}}active{{/isActive}}" href="{{baseUrl}}/{{pathPrefix}}" onclick="setLocalePreference('{{code}}')">{{name}}</a></li>
|
||||
<li><a class="dropdown-item {{#isActive}}active{{/isActive}}" href="{{baseUrl}}/{{pathPrefix}}" onclick="setLocalePreference('{{code}}', this.href); return false;">{{name}}</a></li>
|
||||
{{/availableLocales}}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -59,6 +60,7 @@
|
||||
|
||||
<div class="text-center menu-mobile-top">
|
||||
<a href="{{baseUrl}}/news/" class="fw500 mobile-menu-link">News</a>
|
||||
<a href="{{baseUrl}}/plugins/" class="fw500 mobile-menu-link">Plugins</a>
|
||||
<a href="{{baseUrl}}/help/" class="fw500 mobile-menu-link">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500 mobile-menu-link">Forum</a>
|
||||
</div>
|
||||
@@ -73,7 +75,7 @@
|
||||
<div class="text-center menu-mobile-language">
|
||||
<p class="fw500 mobile-menu-language-label"><i class="fas fa-globe"></i> Language</p>
|
||||
{{#availableLocales}}
|
||||
<a href="{{baseUrl}}/{{pathPrefix}}" class="fw500 mobile-menu-link mobile-language-link {{#isActive}}active{{/isActive}}" onclick="setLocalePreference('{{code}}')">{{name}}</a>
|
||||
<a href="{{baseUrl}}/{{pathPrefix}}" class="fw500 mobile-menu-link mobile-language-link {{#isActive}}active{{/isActive}}" onclick="setLocalePreference('{{code}}', this.href); return false;">{{name}}</a>
|
||||
{{/availableLocales}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- When creating Jest tests, there should be only one `describe()` statement in the file.
|
||||
- Focus on testing essential behaviour and edge cases — avoid adding tests for every minor detail.
|
||||
- Avoid duplicating code in tests; when testing the same logic with different inputs, use `test.each` or shared helpers instead of repeating similar test blocks.
|
||||
- Do not make white space changes - do not add unnecessary new lines, or spaces to existing code, or wrap existing code.
|
||||
|
||||
## Full Documentation
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"version": "latest",
|
||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"git": "2.50.1",
|
||||
"git": "2.51.0",
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
||||
@@ -45,6 +45,10 @@ describe('HtmlToMd', () => {
|
||||
htmlToMdOptions.preserveColorStyles = true;
|
||||
}
|
||||
|
||||
if (htmlFilename.indexOf('table_with') === 0 || htmlFilename.indexOf('table_default') === 0) {
|
||||
htmlToMdOptions.preserveTableStyles = true;
|
||||
}
|
||||
|
||||
const html = await readFile(htmlPath, 'utf8');
|
||||
let expectedMd = await readFile(mdPath, 'utf8');
|
||||
|
||||
|
||||
7
packages/app-cli/tests/enex_to_md/list_with_br.html
Normal file
7
packages/app-cli/tests/enex_to_md/list_with_br.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<ul>
|
||||
<li>First line<br/>Second line</li>
|
||||
<li>Normal item</li>
|
||||
<li>With sub-list<ul>
|
||||
<li>Sub-list<br/>Paragraph<br/>Also another line</li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
8
packages/app-cli/tests/enex_to_md/list_with_br.md
Normal file
8
packages/app-cli/tests/enex_to_md/list_with_br.md
Normal file
@@ -0,0 +1,8 @@
|
||||
- First line
|
||||
Second line
|
||||
|
||||
- Normal item
|
||||
- With sub-list
|
||||
- Sub-list
|
||||
Paragraph
|
||||
Also another line
|
||||
@@ -0,0 +1,18 @@
|
||||
<table style="border-collapse: collapse; width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">Name</th>
|
||||
<th style="width: 50%;">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 50%;">Cell A</td>
|
||||
<td style="width: 50%;">Cell B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 50%;">Cell C</td>
|
||||
<td style="width: 50%;">Cell D</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,4 @@
|
||||
| Name | Value |
|
||||
| --- | --- |
|
||||
| Cell A | Cell B |
|
||||
| Cell C | Cell D |
|
||||
@@ -0,0 +1,18 @@
|
||||
<table bgcolor="#f0f0f0" cellpadding="8">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Cell A</td>
|
||||
<td>Cell B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell C</td>
|
||||
<td>Cell D</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="joplin-table-wrapper"><table bgcolor="#f0f0f0" cellpadding="8"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td>Cell A</td><td>Cell B</td></tr><tr><td>Cell C</td><td>Cell D</td></tr></tbody></table></div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<table style="border-collapse: collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="background-color: #e03e2d">Red cell</td>
|
||||
<td style="padding: 10px 15px">Padded cell</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-color: #2dc26b; border-style: solid">Green border</td>
|
||||
<td>Normal cell</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="joplin-table-wrapper"><table style="border-collapse: collapse"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td style="background-color: #e03e2d">Red cell</td><td style="padding: 10px 15px">Padded cell</td></tr><tr><td style="border-color: #2dc26b; border-style: solid">Green border</td><td>Normal cell</td></tr></tbody></table></div>
|
||||
@@ -6,7 +6,7 @@ const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import { FileLocker } from '@joplin/utils/fs';
|
||||
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme } from 'electron';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme, powerMonitor } from 'electron';
|
||||
import bridge from './bridge';
|
||||
import * as url from 'url';
|
||||
const path = require('path');
|
||||
@@ -401,6 +401,15 @@ export default class ElectronAppWrapper {
|
||||
};
|
||||
addWindowEventHandlers(this.win_.webContents);
|
||||
|
||||
// BrowserWindow 'focus' fires when the OS gives focus to the application window
|
||||
// (i.e. coming from another app or from the taskbar), not on intra-app focus switches.
|
||||
// We use a dedicated IPC channel so the renderer can trigger an immediate sync on
|
||||
// OS-level focus gain without conflating it with the 'window-focused' channel that
|
||||
// handles Joplin-internal window routing.
|
||||
this.win_.on('focus', () => {
|
||||
this.win_?.webContents.send('main-window-focused');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.win_.on('close', (event: any) => {
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
@@ -892,6 +901,11 @@ export default class ElectronAppWrapper {
|
||||
event.preventDefault();
|
||||
void this.openCallbackUrl(url);
|
||||
});
|
||||
|
||||
// When the OS wakes from sleep, notify the renderer so it can trigger an immediate sync.
|
||||
powerMonitor.on('resume', () => {
|
||||
this.win_?.webContents.send('system-resumed');
|
||||
});
|
||||
}
|
||||
|
||||
public async openCallbackUrl(url: string) {
|
||||
|
||||
@@ -733,6 +733,23 @@ class Application extends BaseApplication {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger an immediate sync when the main window gains OS-level focus (i.e. the user
|
||||
// switches back to Joplin from another application) or when the system wakes from sleep.
|
||||
// A 30-second cool-down prevents duplicate syncs during rapid focus-in/focus-out cycles.
|
||||
const minResumeSyncIntervalMs = 30_000;
|
||||
let lastFocusSyncTime = 0;
|
||||
|
||||
const scheduleResumeSync = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastFocusSyncTime > minResumeSyncIntervalMs) {
|
||||
lastFocusSyncTime = now;
|
||||
void reg.scheduleSync(0);
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.on('main-window-focused', scheduleResumeSync);
|
||||
ipcRenderer.on('system-resumed', scheduleResumeSync);
|
||||
});
|
||||
|
||||
addTask('app/initPluginService', () => this.initPluginService());
|
||||
|
||||
@@ -19,7 +19,7 @@ import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/conf
|
||||
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MessageBoxType } from '@joplin/lib/shim';
|
||||
|
||||
|
||||
interface Font {
|
||||
@@ -145,8 +145,16 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
screenName = section.name;
|
||||
|
||||
if (this.hasChanges()) {
|
||||
const ok = await shim.showConfirmationDialog(_('This will open a new screen. Save your current changes?'));
|
||||
if (ok) {
|
||||
const answer = await shim.showMessageBox(
|
||||
_('This will open a new screen. Save your current changes?'),
|
||||
{
|
||||
type: MessageBoxType.Confirm,
|
||||
buttons: [_('Save changes'), _('Discard changes')],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
},
|
||||
);
|
||||
if (answer === 0) {
|
||||
await shared.saveSettings(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,18 @@ describe('useContextMenu', () => {
|
||||
|
||||
it('should return type=image when cursor is inside markdown image', () => {
|
||||
const line = ``;
|
||||
expect(getResourceIdFromMarkup(line, 15)).toEqual({ resourceId, type: 'image' });
|
||||
const result = getResourceIdFromMarkup(line, 15);
|
||||
expect(result.resourceId).toBe(resourceId);
|
||||
expect(result.type).toBe('image');
|
||||
expect(line.substring(result.markupStart, result.markupEnd)).toBe(line);
|
||||
});
|
||||
|
||||
it('should return type=file when cursor is inside markdown link', () => {
|
||||
const line = `[document.pdf](:/${resourceId})`;
|
||||
expect(getResourceIdFromMarkup(line, 15)).toEqual({ resourceId, type: 'file' });
|
||||
const result = getResourceIdFromMarkup(line, 15);
|
||||
expect(result.resourceId).toBe(resourceId);
|
||||
expect(result.type).toBe('file');
|
||||
expect(line.substring(result.markupStart, result.markupEnd)).toBe(line);
|
||||
});
|
||||
|
||||
it('should return null when cursor is outside markup', () => {
|
||||
@@ -22,8 +28,13 @@ describe('useContextMenu', () => {
|
||||
|
||||
it('should correctly distinguish between image and file on same line', () => {
|
||||
const line = ` [file](:/${resourceId2})`;
|
||||
expect(getResourceIdFromMarkup(line, 10)).toEqual({ resourceId, type: 'image' });
|
||||
expect(getResourceIdFromMarkup(line, 48)).toEqual({ resourceId: resourceId2, type: 'file' });
|
||||
const imageResult = getResourceIdFromMarkup(line, 10);
|
||||
expect(imageResult.resourceId).toBe(resourceId);
|
||||
expect(imageResult.type).toBe('image');
|
||||
|
||||
const fileResult = getResourceIdFromMarkup(line, 48);
|
||||
expect(fileResult.resourceId).toBe(resourceId2);
|
||||
expect(fileResult.type).toBe('file');
|
||||
});
|
||||
|
||||
it('should return null for empty line', () => {
|
||||
|
||||
@@ -22,6 +22,8 @@ export type ResourceMarkupType = 'image' | 'file';
|
||||
export interface ResourceMarkupInfo {
|
||||
resourceId: string;
|
||||
type: ResourceMarkupType;
|
||||
markupStart: number;
|
||||
markupEnd: number;
|
||||
}
|
||||
|
||||
// Extract resource ID from resource markup (images or file attachments) at a given cursor position within a line.
|
||||
@@ -74,7 +76,7 @@ export const getResourceIdFromMarkup = (lineContent: string, cursorPosInLine: nu
|
||||
}
|
||||
|
||||
if (markupEnd !== -1 && cursorPosInLine >= markupStart && cursorPosInLine <= markupEnd) {
|
||||
return { resourceId: resourceInfo.itemId, type: markupType };
|
||||
return { resourceId: resourceInfo.itemId, type: markupType, markupStart, markupEnd };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,28 +163,20 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
return clickedElement?.closest(`.${imageClassName}`) as HTMLElement | null;
|
||||
};
|
||||
|
||||
// Get resource info from markup at click position (not cursor position)
|
||||
const getResourceInfoAtClickPos = (params: ContextMenuParams): ResourceMarkupInfo | null => {
|
||||
if (!editorRef.current) return null;
|
||||
|
||||
const editor = editorRef.current.editor;
|
||||
if (!editor) return null;
|
||||
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const x = convertFromScreenCoordinates(zoom, params.x);
|
||||
const y = convertFromScreenCoordinates(zoom, params.y);
|
||||
|
||||
const clickPos = editor.posAtCoords({ x, y });
|
||||
if (clickPos === null) return null;
|
||||
|
||||
const line = editor.state.doc.lineAt(clickPos);
|
||||
return getResourceIdFromMarkup(line.text, clickPos - line.from);
|
||||
};
|
||||
|
||||
const targetWindow = bridge().windowById(windowId);
|
||||
|
||||
const appendEditMenuItems = (menu: typeof Menu.prototype) => {
|
||||
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection();
|
||||
menu.append(new MenuItem({ label: _('Cut'), enabled: hasSelectedText, click: () => props.editorCutText() }));
|
||||
menu.append(new MenuItem({ label: _('Copy'), enabled: hasSelectedText, click: () => props.editorCopyText() }));
|
||||
menu.append(new MenuItem({ label: _('Paste'), enabled: true, click: () => props.editorPaste() }));
|
||||
menu.append(new MenuItem({ label: _('Paste as Markdown'), enabled: true, click: () => CommandService.instance().execute('pasteAsMarkdown') }));
|
||||
};
|
||||
|
||||
const showResourceContextMenu = async (resourceId: string, type: ResourceMarkupType) => {
|
||||
const menu = new Menu();
|
||||
|
||||
// Add resource-specific options first
|
||||
const baseType = type === 'image' ? ContextMenuItemType.Image : ContextMenuItemType.Resource;
|
||||
const itemType = await resolveContextMenuItemType(baseType, resourceId);
|
||||
const contextMenuOptions: ContextMenuOptions = {
|
||||
@@ -194,18 +188,34 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
linkToOpen: null,
|
||||
textToCopy: null,
|
||||
htmlToCopy: null,
|
||||
insertContent: () => {},
|
||||
isReadOnly: true,
|
||||
insertContent: () => { editorRef.current?.insertText(''); },
|
||||
isReadOnly: false,
|
||||
fireEditorEvent: () => {},
|
||||
htmlToMd: null,
|
||||
mdToHtml: null,
|
||||
};
|
||||
|
||||
const resourceMenuItems = await buildMenuItems(menuItems(props.dispatch), contextMenuOptions);
|
||||
const resourceMenuItems = await buildMenuItems(menuItems(props.dispatch), contextMenuOptions, { excludeEditItems: true, excludePluginItems: true });
|
||||
for (const item of resourceMenuItems) {
|
||||
menu.append(item);
|
||||
}
|
||||
|
||||
// Add edit items
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
appendEditMenuItems(menu);
|
||||
|
||||
// Add plugin items last
|
||||
const extraItems = await handleEditorContextMenuFilter({
|
||||
resourceId,
|
||||
itemType,
|
||||
});
|
||||
if (extraItems.length) {
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
for (const item of extraItems) {
|
||||
menu.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
menu.popup({ window: targetWindow });
|
||||
};
|
||||
|
||||
@@ -227,7 +237,25 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
interface ResourceContextInfo {
|
||||
resourceId: string;
|
||||
type: ResourceMarkupType;
|
||||
}
|
||||
|
||||
const getResourceInfoAtPos = (docPos: number): ResourceContextInfo | null => {
|
||||
const editor = editorRef.current?.editor;
|
||||
if (!editor) return null;
|
||||
|
||||
const line = editor.state.doc.lineAt(docPos);
|
||||
const info = getResourceIdFromMarkup(line.text, docPos - line.from);
|
||||
if (!info) return null;
|
||||
|
||||
return { resourceId: info.resourceId, type: info.type };
|
||||
};
|
||||
|
||||
const onContextMenu = async (event: Event, params: ContextMenuParams) => {
|
||||
let resourceInfo: ResourceContextInfo | null = null;
|
||||
|
||||
// Check if right-clicking on a rendered image first (images may not be "editable")
|
||||
const imageContainer = getClickedImageContainer(params);
|
||||
if (imageContainer && pointerInsideEditor(params, true)) {
|
||||
@@ -235,19 +263,40 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
if (imgElement) {
|
||||
const resourceId = pathToId(imgElement.src);
|
||||
if (resourceId) {
|
||||
event.preventDefault();
|
||||
moveCursorToImageLine(imageContainer);
|
||||
await showResourceContextMenu(resourceId, 'image');
|
||||
return;
|
||||
const sourceFrom = imageContainer.dataset.sourceFrom;
|
||||
if (sourceFrom !== undefined) {
|
||||
const editor = editorRef.current?.editor;
|
||||
if (editor) {
|
||||
const pos = Math.min(Number(sourceFrom), editor.state.doc.length);
|
||||
resourceInfo = getResourceInfoAtPos(pos);
|
||||
}
|
||||
}
|
||||
// Fallback if we couldn't get markup info
|
||||
if (!resourceInfo) {
|
||||
resourceInfo = { resourceId, type: 'image' };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if right-clicking on resource markup text (images or file attachments)
|
||||
const markupResourceInfo = getResourceInfoAtClickPos(params);
|
||||
if (markupResourceInfo && pointerInsideEditor(params)) {
|
||||
if (!resourceInfo && pointerInsideEditor(params)) {
|
||||
const editor = editorRef.current?.editor;
|
||||
if (editor) {
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const x = convertFromScreenCoordinates(zoom, params.x);
|
||||
const y = convertFromScreenCoordinates(zoom, params.y);
|
||||
const clickPos = editor.posAtCoords({ x, y });
|
||||
if (clickPos !== null) {
|
||||
resourceInfo = getResourceInfoAtPos(clickPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceInfo) {
|
||||
event.preventDefault();
|
||||
await showResourceContextMenu(markupResourceInfo.resourceId, markupResourceInfo.type);
|
||||
await showResourceContextMenu(resourceInfo.resourceId, resourceInfo.type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,48 +307,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
event.preventDefault();
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Cut'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
props.editorCutText();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Copy'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
props.editorCopyText();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Paste'),
|
||||
enabled: true,
|
||||
click: async () => {
|
||||
props.editorPaste();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Paste as Markdown'),
|
||||
enabled: true,
|
||||
click: async () => {
|
||||
await CommandService.instance().execute('pasteAsMarkdown');
|
||||
},
|
||||
}),
|
||||
);
|
||||
appendEditMenuItems(menu);
|
||||
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||
|
||||
|
||||
@@ -221,7 +221,14 @@ const translateLE_ = (codeMirror: any, percent: number, l2e: boolean) => {
|
||||
linInterp = percent * lineCount - lineU;
|
||||
result = ePercentU + (ePercentL - ePercentU) * linInterp;
|
||||
} else {
|
||||
linInterp = Math.max(0, Math.min(1, (percent - ePercentU) / (ePercentL - ePercentU))) || 0;
|
||||
const rawLinInterp = (percent - ePercentU) / (ePercentL - ePercentU);
|
||||
if (ePercentL === ePercentU) {
|
||||
// Prevents the Viewer from jumping to the bottom of
|
||||
// the document when there is division by zero.
|
||||
linInterp = percent;
|
||||
} else {
|
||||
linInterp = Math.max(0, Math.min(1, rawLinInterp)) || 0;
|
||||
}
|
||||
result = (lineU + linInterp) / lineCount;
|
||||
}
|
||||
return Math.max(0, Math.min(1, result));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef, useContext } from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, ForwardedRef, useContext } from 'react';
|
||||
|
||||
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
|
||||
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||
@@ -12,11 +12,10 @@ import Note from '@joplin/lib/models/Note';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import { clipboard } from 'electron';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||
import { EditorKeymap, EditorLanguageType, EditorSettings, SearchState, UserEventSource } from '@joplin/editor/types';
|
||||
import { SearchState, UserEventSource } from '@joplin/editor/types';
|
||||
import useStyles from '../utils/useStyles';
|
||||
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
||||
import useScrollHandler from '../utils/useScrollHandler';
|
||||
@@ -33,6 +32,7 @@ import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
|
||||
import useSyncEditorValue from './utils/useSyncEditorValue';
|
||||
import { getGlobalSettings } from '@joplin/renderer/types';
|
||||
import useEditorSettings from './utils/useEditorSettings';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -338,47 +338,6 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
}, []);
|
||||
|
||||
const editorSettings = useMemo((): EditorSettings => {
|
||||
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
|
||||
|
||||
let keyboardMode = EditorKeymap.Default;
|
||||
if (props.keyboardMode === 'vim') {
|
||||
keyboardMode = EditorKeymap.Vim;
|
||||
} else if (props.keyboardMode === 'emacs') {
|
||||
keyboardMode = EditorKeymap.Emacs;
|
||||
}
|
||||
|
||||
return {
|
||||
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
|
||||
readOnly: props.disabled,
|
||||
markdownMarkEnabled: Setting.value('markdown.plugin.mark'),
|
||||
markdownInsertEnabled: Setting.value('markdown.plugin.insert'),
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
inlineRenderingEnabled: Setting.value('editor.inlineRendering'),
|
||||
imageRenderingEnabled: Setting.value('editor.imageRendering'),
|
||||
highlightActiveLine: Setting.value('editor.highlightActiveLine'),
|
||||
themeData: {
|
||||
...styles.globalTheme,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
|
||||
},
|
||||
automatchBraces: Setting.value('editor.autoMatchingBraces'),
|
||||
autocompleteMarkup: Setting.value('editor.autocompleteMarkup'),
|
||||
useExternalSearch: false,
|
||||
ignoreModifiers: true,
|
||||
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
||||
keymap: keyboardMode,
|
||||
preferMacShortcuts: shim.isMac(),
|
||||
indentWithTabs: true,
|
||||
tabMovesFocus: props.tabMovesFocus,
|
||||
editorLabel: _('Markdown editor'),
|
||||
};
|
||||
}, [
|
||||
props.contentMarkupLanguage, props.disabled, props.keyboardMode, styles.globalTheme,
|
||||
props.tabMovesFocus,
|
||||
]);
|
||||
|
||||
const initialCursorLocationRef = useRef(0);
|
||||
initialCursorLocationRef.current = props.initialCursorLocation.markdown ?? 0;
|
||||
|
||||
@@ -391,6 +350,14 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
initialCursorLocationRef,
|
||||
});
|
||||
|
||||
const settings = useEditorSettings({
|
||||
baseTheme: styles.globalTheme,
|
||||
contentMarkupLanguage: props.contentMarkupLanguage,
|
||||
disabled: props.disabled,
|
||||
keyboardMode: props.keyboardMode,
|
||||
tabMovesFocus: props.tabMovesFocus,
|
||||
});
|
||||
|
||||
const renderEditor = () => {
|
||||
return (
|
||||
<div className='editor'>
|
||||
@@ -400,7 +367,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
initialSelectionRef={initialCursorLocationRef}
|
||||
initialNoteId={props.noteId}
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
settings={settings}
|
||||
pluginStates={props.plugins}
|
||||
onPasteFile={null}
|
||||
onEvent={onEditorEvent}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { EditorKeymap, EditorLanguageType, EditorSettings, EditorTheme } from '@joplin/editor/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from '../../../../../../app.reducer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
interface EditorSettingsProps {
|
||||
contentMarkupLanguage: MarkupLanguage;
|
||||
keyboardMode: string;
|
||||
disabled: boolean;
|
||||
tabMovesFocus: boolean;
|
||||
baseTheme: EditorTheme;
|
||||
}
|
||||
|
||||
const useEditorSettings = (props: EditorSettingsProps) => {
|
||||
const stateToSettings = (state: AppState) => ({
|
||||
markdownMark: state.settings['markdown.plugin.mark'],
|
||||
markdownInsert: state.settings['markdown.plugin.insert'],
|
||||
katex: state.settings['markdown.plugin.katex'],
|
||||
inlineRendering: state.settings['editor.inlineRendering'],
|
||||
imageRendering: state.settings['editor.imageRendering'],
|
||||
highlightActiveLine: state.settings['editor.highlightActiveLine'],
|
||||
monospaceFont: state.settings['style.editor.monospaceFontFamily'],
|
||||
automatchBraces: state.settings['editor.autoMatchingBraces'],
|
||||
autocompleteMarkup: state.settings['editor.autocompleteMarkup'],
|
||||
spellcheckEnabled: state.settings['editor.spellcheckBeta'],
|
||||
});
|
||||
type SelectedSettings = ReturnType<typeof stateToSettings>;
|
||||
const settings = useSelector<AppState, SelectedSettings>(stateToSettings, isDeepStrictEqual);
|
||||
|
||||
return useMemo((): EditorSettings => {
|
||||
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
|
||||
|
||||
let keyboardMode = EditorKeymap.Default;
|
||||
if (props.keyboardMode === 'vim') {
|
||||
keyboardMode = EditorKeymap.Vim;
|
||||
} else if (props.keyboardMode === 'emacs') {
|
||||
keyboardMode = EditorKeymap.Emacs;
|
||||
}
|
||||
|
||||
return {
|
||||
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
|
||||
readOnly: props.disabled,
|
||||
markdownMarkEnabled: settings.markdownMark,
|
||||
markdownInsertEnabled: settings.markdownInsert,
|
||||
katexEnabled: settings.katex,
|
||||
inlineRenderingEnabled: settings.inlineRendering,
|
||||
imageRenderingEnabled: settings.imageRendering,
|
||||
highlightActiveLine: settings.highlightActiveLine,
|
||||
themeData: {
|
||||
...props.baseTheme,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
monospaceFont: settings.monospaceFont,
|
||||
},
|
||||
automatchBraces: settings.automatchBraces,
|
||||
autocompleteMarkup: settings.autocompleteMarkup,
|
||||
useExternalSearch: false,
|
||||
ignoreModifiers: true,
|
||||
spellcheckEnabled: settings.spellcheckEnabled,
|
||||
keymap: keyboardMode,
|
||||
preferMacShortcuts: shim.isMac(),
|
||||
indentWithTabs: true,
|
||||
tabMovesFocus: props.tabMovesFocus,
|
||||
editorLabel: _('Markdown editor'),
|
||||
};
|
||||
}, [
|
||||
props.contentMarkupLanguage, props.disabled, props.keyboardMode, props.baseTheme,
|
||||
props.tabMovesFocus, settings,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useEditorSettings;
|
||||
@@ -896,6 +896,30 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
editor.addShortcut('Meta+Shift+8', '', () => editor.execCommand('InsertUnorderedList'));
|
||||
editor.addShortcut('Meta+Shift+9', '', () => editor.execCommand('InsertJoplinChecklist'));
|
||||
|
||||
// Override ScrollIntoView to scroll to the cursor's character position
|
||||
// instead of the start of the paragraph.
|
||||
// See: https://github.com/laurent22/joplin/issues/14143
|
||||
editor.on('ScrollIntoView', (event) => {
|
||||
const sel = editor.getDoc().getSelection();
|
||||
if (!sel || sel.rangeCount === 0) return;
|
||||
|
||||
const rect = sel.getRangeAt(0).getBoundingClientRect();
|
||||
const win = editor.getWin();
|
||||
const viewHeight = win.innerHeight;
|
||||
|
||||
if (rect.top < 0) {
|
||||
win.scrollBy(0, rect.top);
|
||||
} else if (rect.bottom > viewHeight) {
|
||||
win.scrollBy(0, rect.bottom - viewHeight);
|
||||
} else if (rect.top === 0 && rect.height === 0) {
|
||||
// Handles edge case where rect is not rendered
|
||||
// See: https://stackoverflow.com/a/14384220/5757550
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
return;
|
||||
});
|
||||
|
||||
// TODO: remove event on unmount?
|
||||
editor.on('drop', (event) => {
|
||||
// Prevent the message "Dropped file type is not supported" from showing up.
|
||||
@@ -1335,13 +1359,35 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onSetAttrib = (event: EditorEvent<any>) => {
|
||||
// Dispatch onChange when a link is edited
|
||||
// Dispatch onChange when a link or table-related formatting is edited
|
||||
const target = Array.isArray(event.attrElm) ? event.attrElm[0] : event.attrElm;
|
||||
if (!target) return;
|
||||
|
||||
if (target.nodeName === 'A') {
|
||||
if (event.attrName === 'title' || event.attrName === 'href' || event.attrName === 'rel') {
|
||||
onChangeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
if (['TABLE', 'TR', 'TD', 'TH'].includes(target.nodeName)) {
|
||||
const attributeName = (event.attrName ?? '').toLowerCase();
|
||||
if (
|
||||
attributeName === 'style' ||
|
||||
attributeName === 'class' ||
|
||||
attributeName === 'bgcolor' ||
|
||||
attributeName === 'bordercolor' ||
|
||||
attributeName === 'background' ||
|
||||
attributeName === 'cellpadding' ||
|
||||
attributeName === 'cellspacing'
|
||||
) {
|
||||
onChangeHandler();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Table plugin fires this on structure/style changes from dialogs.
|
||||
const onTableModified = () => {
|
||||
onChangeHandler();
|
||||
};
|
||||
|
||||
// Keypress means that a printable key (letter, digit, etc.) has been
|
||||
@@ -1490,6 +1536,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
editor.on(TinyMceEditorEvents.Redo, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.ExecCommand, onExecCommand);
|
||||
editor.on(TinyMceEditorEvents.SetAttrib, onSetAttrib);
|
||||
editor.on('TableModified', onTableModified);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
@@ -1506,6 +1553,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
editor.off(TinyMceEditorEvents.Redo, onChangeHandler);
|
||||
editor.off(TinyMceEditorEvents.ExecCommand, onExecCommand);
|
||||
editor.off(TinyMceEditorEvents.SetAttrib, onSetAttrib);
|
||||
editor.off('TableModified', onTableModified);
|
||||
} catch (error) {
|
||||
console.warn('Error removing events', error);
|
||||
}
|
||||
|
||||
@@ -197,39 +197,48 @@ export const handleEditorContextMenuFilter = async (context?: EditorContextMenuF
|
||||
return output;
|
||||
};
|
||||
|
||||
export const buildMenuItems = async (items: ContextMenuItems, options: ContextMenuOptions) => {
|
||||
export interface BuildMenuItemsOptions {
|
||||
excludeEditItems?: boolean;
|
||||
excludePluginItems?: boolean;
|
||||
}
|
||||
|
||||
export const buildMenuItems = async (items: ContextMenuItems, options: ContextMenuOptions, buildOptions?: BuildMenuItemsOptions) => {
|
||||
const editItemKeys = ['cut', 'copy', 'paste', 'pasteAsText', 'separator4'];
|
||||
const activeItems: ContextMenuItem[] = [];
|
||||
for (const itemKey in items) {
|
||||
if (buildOptions?.excludeEditItems && editItemKeys.includes(itemKey)) continue;
|
||||
const item = items[itemKey];
|
||||
if (item.isActive(options.itemType, options)) {
|
||||
activeItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const extraItems = await handleEditorContextMenuFilter({
|
||||
resourceId: options.resourceId,
|
||||
itemType: options.itemType,
|
||||
textToCopy: options.textToCopy,
|
||||
});
|
||||
|
||||
if (extraItems.length) {
|
||||
activeItems.push({
|
||||
isActive: () => true,
|
||||
label: '',
|
||||
onAction: () => {},
|
||||
isSeparator: true,
|
||||
if (!buildOptions?.excludePluginItems) {
|
||||
const extraItems = await handleEditorContextMenuFilter({
|
||||
resourceId: options.resourceId,
|
||||
itemType: options.itemType,
|
||||
textToCopy: options.textToCopy,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [, extraItem] of extraItems.entries()) {
|
||||
activeItems.push({
|
||||
isActive: () => true,
|
||||
label: extraItem.label,
|
||||
onAction: () => {
|
||||
extraItem.click();
|
||||
},
|
||||
isSeparator: extraItem.type === 'separator',
|
||||
});
|
||||
if (extraItems.length) {
|
||||
activeItems.push({
|
||||
isActive: () => true,
|
||||
label: '',
|
||||
onAction: () => {},
|
||||
isSeparator: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [, extraItem] of extraItems.entries()) {
|
||||
activeItems.push({
|
||||
isActive: () => true,
|
||||
label: extraItem.label,
|
||||
onAction: () => {
|
||||
extraItem.click();
|
||||
},
|
||||
isSeparator: extraItem.type === 'separator',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = filterSeparators(activeItems, item => item.isSeparator);
|
||||
|
||||
@@ -13,6 +13,7 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
|
||||
newBody = htmlToMd.parse(html, {
|
||||
preserveImageTagsWithSize: true,
|
||||
preserveNestedTables: true,
|
||||
preserveTableStyles: true,
|
||||
preserveColorStyles: true,
|
||||
...parseOptions,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { processPastedHtml } from './resourceHandling';
|
||||
import { processImagesInPastedHtml, processPastedHtml } from './resourceHandling';
|
||||
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||
import HtmlToMd from '@joplin/lib/HtmlToMd';
|
||||
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
const createTestMarkupConverters = () => {
|
||||
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
|
||||
@@ -111,4 +112,21 @@ describe('resourceHandling', () => {
|
||||
expect(result).not.toMatch(/alt="[^"]*\n/);
|
||||
}
|
||||
});
|
||||
|
||||
// Regression test: base64 branch was hardcoding file:// and ignoring useInternalUrls
|
||||
// 1x1 transparent PNG — smallest valid base64-encoded image for testing
|
||||
const minimalPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||
|
||||
test.each([
|
||||
{ useInternalUrls: true, expectMatch: /src=":\/[a-f0-9]+"/, expectAbsent: 'file://' },
|
||||
{ useInternalUrls: false, expectMatch: /src="file:\/\//, expectAbsent: 'data:' },
|
||||
])('should convert base64 image using resourceUrl (useInternalUrls=$useInternalUrls)', async ({ useInternalUrls, expectMatch, expectAbsent }) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
const html = `<img src="data:image/png;base64,${minimalPng}"/>`;
|
||||
const result = await processImagesInPastedHtml(html, { useInternalUrls });
|
||||
expect(result).toMatch(expectMatch);
|
||||
expect(result).not.toContain(expectAbsent);
|
||||
expect(result).not.toContain('data:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,7 +185,7 @@ export const processImagesInPastedHtml = async (html: string, options: ProcessIm
|
||||
// Word encodes base64 with MIME line breaks every ~76 chars.
|
||||
// Strip whitespace before decoding, then save as a Joplin resource
|
||||
// so Turndown's outerHTML (used for images with width/height) gets
|
||||
// a short file:// URL instead of 200KB of base64.
|
||||
// a short URL instead of 200KB of base64.
|
||||
const cleanSrc = imageSrc.replace(/\s/g, '');
|
||||
const dataUrlMatch = cleanSrc.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (dataUrlMatch) {
|
||||
@@ -196,7 +196,7 @@ export const processImagesInPastedHtml = async (html: string, options: ProcessIm
|
||||
try {
|
||||
await shim.fsDriver().writeFile(filePath, base64Data, 'base64');
|
||||
const createdResource = await shim.createResourceFromPath(filePath);
|
||||
mappedResources[imageSrc] = `file://${encodeURI(Resource.fullPath(createdResource))}`;
|
||||
mappedResources[imageSrc] = resourceUrl(createdResource);
|
||||
} catch (writeError) {
|
||||
writeError.message = `processPastedHtml: Failed to write or create resource from pasted image: ${writeError.message}`;
|
||||
throw writeError;
|
||||
|
||||
@@ -284,9 +284,11 @@ interface ConnectProps {
|
||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
const hasFolderForNewNotes = whenClauseContext.selectedFolderIsValid
|
||||
&& windowState.selectedFolderId !== getTrashFolderId();
|
||||
|
||||
return {
|
||||
showNewNoteButtons: windowState.selectedFolderId !== getTrashFolderId(),
|
||||
showNewNoteButtons: hasFolderForNewNotes,
|
||||
newNoteButtonEnabled: CommandService.instance().isEnabled('newNote', whenClauseContext),
|
||||
newTodoButtonEnabled: CommandService.instance().isEnabled('newTodo', whenClauseContext),
|
||||
sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'],
|
||||
|
||||
@@ -60,7 +60,7 @@ const useNoteListControlsBreakpoints = (width: number, newNoteButtonElement: Ele
|
||||
const [dynamicBreakpoints, setDynamicBreakpoints] = useState<Breakpoints>({ Sm: BaseBreakpoint.Sm, Md: BaseBreakpoint.Md, Lg: BaseBreakpoint.Lg, Xl: BaseBreakpoint.Xl });
|
||||
const previousWidth = usePrevious(width);
|
||||
const widthHasChanged = width !== previousWidth;
|
||||
const showNewNoteButton = selectedFolderId !== getTrashFolderId();
|
||||
const showNewNoteButton = !!selectedFolderId && selectedFolderId !== getTrashFolderId();
|
||||
|
||||
// Initialize language-specific breakpoints
|
||||
useEffect(() => {
|
||||
|
||||
@@ -157,7 +157,10 @@ const NoteRevisionViewerComponent: React.FC<Props> = ({ themeId, noteId, onBack,
|
||||
// if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, args);
|
||||
|
||||
try {
|
||||
if (msg.indexOf('joplin://') === 0) {
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
// Revision previews are read-only. Ignore checkbox toggle IPC messages so they
|
||||
// don't fall through to URL handling (`checkboxclick:` looks like a protocol).
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
throw new Error(_('Unsupported link or message: %s', msg));
|
||||
} else if (urlUtils.urlProtocol(msg)) {
|
||||
await bridge().openExternal(msg);
|
||||
|
||||
@@ -27,6 +27,7 @@ interface Props {
|
||||
dispatch?: Function;
|
||||
selectedNoteId: string;
|
||||
isFocused?: boolean;
|
||||
globalQuery?: string;
|
||||
}
|
||||
|
||||
function SearchBar(props: Props) {
|
||||
@@ -163,6 +164,22 @@ function SearchBar(props: Props) {
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
// if the globalQuery is not undefined and is not equal to the current value of query changes the query to global query. Else the current query remains the same.
|
||||
// used setQuery((previousQuery)=>{}) to prevent linter error asking to have [query] in the dependency array, since this useEffect would then run every time the query is changed
|
||||
useEffect(() => {
|
||||
if (props.globalQuery !== undefined) {
|
||||
setQuery((previousQuery) => {
|
||||
if (props.globalQuery !== previousQuery) {
|
||||
if (props.globalQuery.length > 0) {
|
||||
setSearchStarted(true);
|
||||
}
|
||||
return props.globalQuery;
|
||||
}
|
||||
return previousQuery;
|
||||
});
|
||||
}
|
||||
}, [props.globalQuery]);
|
||||
|
||||
return (
|
||||
<Root className="search-bar">
|
||||
<SearchInput
|
||||
@@ -186,10 +203,20 @@ interface OwnProps {
|
||||
|
||||
const mapStateToProps = (state: AppState, ownProps: OwnProps) => {
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
|
||||
let globalQuery = '';
|
||||
if (windowState.notesParentType === 'Search' && windowState.selectedSearchId) {
|
||||
const activeSearch = state.searches.find((s: { id: string; query_pattern: string }) => s.id === windowState.selectedSearchId);
|
||||
if (activeSearch && activeSearch.query_pattern) {
|
||||
globalQuery = activeSearch.query_pattern;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
notesParentType: windowState.notesParentType,
|
||||
selectedNoteId: stateUtils.selectedNoteId(windowState),
|
||||
isFocused: state.focusedField === 'globalSearch',
|
||||
globalQuery: globalQuery,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
export const newNoteEnabledConditions = 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly && !folderIsTrash';
|
||||
export const newNoteEnabledConditions = 'oneFolderSelected && selectedFolderIsValid && !inConflictFolder && !folderIsReadOnly && !folderIsTrash';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'newNote',
|
||||
|
||||
@@ -34,13 +34,14 @@ export default class MainScreen {
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.newNoteButton.waitFor();
|
||||
await this.noteList.waitFor();
|
||||
}
|
||||
|
||||
// Follows the steps a user would use to create a new note.
|
||||
public async createNewNote(title: string) {
|
||||
await this.waitFor();
|
||||
// The new note button is only visible when a folder is selected -- wait for it explicitly.
|
||||
await this.newNoteButton.waitFor();
|
||||
|
||||
// Create the new note. Retry this -- creating new notes can sometimes fail if done just after
|
||||
// application startup.
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.8",
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"esbuild": "^0.26.0",
|
||||
"formatcoords": "1.1.3",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "4.0.2",
|
||||
|
||||
@@ -27,6 +27,7 @@ function useCss(themeId: number, editorCss: string): string {
|
||||
|
||||
:root {
|
||||
background-color: ${theme.backgroundColor};
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -41,7 +42,6 @@ function useCss(themeId: number, editorCss: string): string {
|
||||
padding-bottom: 1px;
|
||||
padding-top: 10px;
|
||||
|
||||
font-size: 13pt;
|
||||
font-family: ${JSON.stringify(theme.fontFamily)}, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const convertHtmlToMarkdown = (html: string|HTMLElement) => {
|
||||
codeBlockStyle: 'fenced',
|
||||
preserveImageTagsWithSize: true,
|
||||
preserveNestedTables: true,
|
||||
preserveTableStyles: true,
|
||||
preserveColorStyles: true,
|
||||
bulletListMarker: '-',
|
||||
emDelimiter: '*',
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.21.2",
|
||||
"esbuild": "0.25.12",
|
||||
"esbuild": "0.26.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"gulp": "4.0.2",
|
||||
|
||||
@@ -19,7 +19,7 @@ import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
|
||||
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
|
||||
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
|
||||
import { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } from 'react-native';
|
||||
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
|
||||
import { AppState as RNAppState, AppStateStatus, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
|
||||
import getResponsiveValue from './components/getResponsiveValue';
|
||||
import NetInfo, { NetInfoSubscription } from '@react-native-community/netinfo';
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
@@ -295,7 +295,8 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
private unsubscribeScreenWidthChangeHandler_: EmitterSubscription|undefined;
|
||||
private unsubscribeNetInfoHandler_: NetInfoSubscription|undefined;
|
||||
private unsubscribeNewShareListener_: UnsubscribeShareListener|undefined;
|
||||
private onAppStateChange_: ()=> void;
|
||||
private onAppStateChange_: (nextAppState: AppStateStatus)=> void;
|
||||
private lastResumeSyncTime_ = 0;
|
||||
private backButtonHandler_: BackButtonHandler;
|
||||
private handleNewShare_: ()=> void;
|
||||
private handleOpenURL_: (event: unknown)=> void;
|
||||
@@ -315,8 +316,24 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
return this.backButtonHandler();
|
||||
};
|
||||
|
||||
this.onAppStateChange_ = () => {
|
||||
this.onAppStateChange_ = (nextAppState: AppStateStatus) => {
|
||||
PoorManIntervals.update();
|
||||
|
||||
// Trigger sync immediately when the app becomes active (resume from background/lock screen).
|
||||
// Only run when the app becomes active, with a 30-second minimum interval
|
||||
// prevent sync spam on rapid lock/unlock cycles.
|
||||
const minResumeSyncIntervalMs = 30_000;
|
||||
if (nextAppState === 'active') {
|
||||
const elapsed = Date.now() - this.lastResumeSyncTime_;
|
||||
if (elapsed >= minResumeSyncIntervalMs) {
|
||||
logger.info(`onAppStateChange_: App became active - scheduling immediate sync (elapsed since last resume sync: ${elapsed}ms)`);
|
||||
this.lastResumeSyncTime_ = Date.now();
|
||||
|
||||
void reg.scheduleSync(0, null, true);
|
||||
} else {
|
||||
logger.info(`onAppStateChange_: App became active but skipping sync - minimum interval not reached (${elapsed}ms < ${minResumeSyncIntervalMs}ms)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -207,6 +207,12 @@ const config = {
|
||||
label: 'News',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
to: process.env.WEBSITE_BASE_URL + '/plugins',
|
||||
label: 'Plugins',
|
||||
position: 'right',
|
||||
target: '_self',
|
||||
},
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'helpSidebar',
|
||||
|
||||
@@ -210,4 +210,47 @@ describe('CodeMirror5Emulation', () => {
|
||||
{ line: 0, ch: 5 },
|
||||
)).toThrow(/is not an integer/i);
|
||||
});
|
||||
|
||||
it('heightAtLine for a line past the document should be greater than for the last line', () => {
|
||||
const codeMirror = makeCodeMirrorEmulation('line1\nline2\nline3');
|
||||
|
||||
// Mock lineBlockAt to return a block with non-zero height so that the
|
||||
// distinction between top and top+height is observable in the test.
|
||||
const mockLineBlock = { top: 900, bottom: 1000, height: 100, from: 0, to: 0 };
|
||||
jest.spyOn(codeMirror.editor, 'lineBlockAt').mockReturnValue(mockLineBlock as never);
|
||||
|
||||
const lineCount = codeMirror.lineCount();
|
||||
|
||||
const heightAtLastLine = codeMirror.heightAtLine(lineCount - 1, 'local');
|
||||
const heightPastEnd = codeMirror.heightAtLine(lineCount, 'local');
|
||||
|
||||
expect(heightPastEnd).toBeGreaterThan(heightAtLastLine);
|
||||
});
|
||||
|
||||
it('heightAtLine for a line past the document should be greater than for the last line, given one long line', () => {
|
||||
const singleLine = 'Very long line of text. '.repeat(400);
|
||||
const codeMirror = makeCodeMirrorEmulation(`${singleLine}`);
|
||||
|
||||
// Mock lineBlockAt to return a block with non-zero height so that the
|
||||
// distinction between top and top+height is observable in the test.
|
||||
const mockLineBlock = { top: 900, bottom: 1000, height: 100, from: 0, to: 0 };
|
||||
jest.spyOn(codeMirror.editor, 'lineBlockAt').mockReturnValue(mockLineBlock as never);
|
||||
|
||||
const lineCount = codeMirror.lineCount();
|
||||
|
||||
const heightAtLastLine = codeMirror.heightAtLine(lineCount - 1, 'local');
|
||||
const heightPastEnd = codeMirror.heightAtLine(lineCount, 'local');
|
||||
|
||||
expect(heightPastEnd).toBeGreaterThan(heightAtLastLine);
|
||||
});
|
||||
|
||||
it('heightAtLine should return a non-negative value for valid line numbers', () => {
|
||||
const codeMirror = makeCodeMirrorEmulation('first\nsecond\nthird');
|
||||
const lineCount = codeMirror.lineCount();
|
||||
|
||||
// Test all lines from top to bottom of document
|
||||
for (let i = 0; i <= lineCount; i++) {
|
||||
expect(codeMirror.heightAtLine(i, 'local')).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -268,7 +268,15 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
|
||||
const lineInfo = doc.line(Math.min(lineNumber + 1, doc.lines));
|
||||
const lineBlock = this.editor.lineBlockAt(lineInfo.from);
|
||||
|
||||
const height = lineBlock.top;
|
||||
let height;
|
||||
if (lineNumber >= doc.lines) {
|
||||
// Handle case when lineNumber is at or below the last line
|
||||
// This ensures ePercentL != ePercentU in translateLE_, which may cause linInterp to be infinity.
|
||||
// See: https://github.com/laurent22/joplin/issues/14143#issuecomment-3767793559
|
||||
height = lineBlock.top + lineBlock.height;
|
||||
} else {
|
||||
height = lineBlock.top;
|
||||
}
|
||||
if (mode === 'local') {
|
||||
const editorTop = this.editor.lineBlockAt(0).top;
|
||||
return height - editorTop;
|
||||
|
||||
@@ -27,4 +27,21 @@ left | right
|
||||
expect(codeBlock.textContent).toBe('`foo`');
|
||||
expect(codeBlock.parentElement.classList.contains('.cm-tableRow'));
|
||||
});
|
||||
|
||||
test.each([
|
||||
0,
|
||||
'before ++'.length + 1,
|
||||
])('should decorate ++insert++ spans when the caret is at %i', async cursorPos => {
|
||||
const editorText = 'before ++inserted++ after';
|
||||
const editor = await createTestEditor(
|
||||
editorText,
|
||||
EditorSelection.cursor(cursorPos),
|
||||
['Insert'],
|
||||
[decoratorExtension],
|
||||
);
|
||||
|
||||
const insertSpan = editor.contentDOM.querySelector('.cm-insert');
|
||||
expect(insertSpan).not.toBeNull();
|
||||
expect(insertSpan?.textContent).toContain('inserted');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,6 +116,10 @@ const strikethroughDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-strike' },
|
||||
});
|
||||
|
||||
const insertDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-insert' },
|
||||
});
|
||||
|
||||
const nodeNameToLineDecoration: Record<string, Decoration> = {
|
||||
'FencedCode': codeBlockDecoration,
|
||||
'CodeBlock': codeBlockDecoration,
|
||||
@@ -150,6 +154,7 @@ const nodeNameToMarkDecoration: Record<string, Decoration> = {
|
||||
'HorizontalRule': horizontalRuleDecoration,
|
||||
'TaskMarker': taskMarkerDecoration,
|
||||
'Strikethrough': strikethroughDecoration,
|
||||
'Insert': insertDecoration,
|
||||
'Highlight': markDecoration,
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,17 @@ describe('MarkdownFrontMatterExtension', () => {
|
||||
expect(frontMatterNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should treat the entire document as frontmatter when closing delimiter is missing (issue #14542)', async () => {
|
||||
const documentText = '---\nsome: frontmatter\n--\n\n# Hey';
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName]);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
|
||||
// Frontmatter block must be recognised and span the entire document
|
||||
expect(frontMatterNodes.length).toBe(1);
|
||||
expect(frontMatterNodes[0].from).toBe(0);
|
||||
expect(frontMatterNodes[0].to).toBe(documentText.length);
|
||||
});
|
||||
|
||||
it('should handle empty FrontMatter block', async () => {
|
||||
const documentText = '---\n---\n\n# Heading';
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName, 'ATXHeading1']);
|
||||
|
||||
@@ -66,14 +66,15 @@ const frontMatterConfig: MarkdownConfig = {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store the opening delimiter position
|
||||
// If the document starts with --- always claim it as a frontmatter block,
|
||||
// even when the closing delimiter is absent.
|
||||
const openingMarkerStart = cx.lineStart;
|
||||
const openingMarkerEnd = cx.lineStart + line.text.length;
|
||||
const contentStart = openingMarkerEnd + 1;
|
||||
|
||||
const contentStart = openingMarkerEnd + 1; // Start after the opening --- and newline
|
||||
let foundEnd = false;
|
||||
|
||||
// Consume lines until we find the closing ---
|
||||
// Consume lines until we find the closing --- or reach end of document.
|
||||
while (cx.nextLine()) {
|
||||
if (frontMatterDelimiterRegex.test(line.text)) {
|
||||
foundEnd = true;
|
||||
@@ -81,37 +82,34 @@ const frontMatterConfig: MarkdownConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundEnd) {
|
||||
// No closing delimiter found - not a valid FrontMatter block
|
||||
return false;
|
||||
}
|
||||
// cx.lineStart now points to the closing --- (if found) or end of document (if not).
|
||||
const contentEnd = cx.lineStart;
|
||||
|
||||
// The content is between the two --- delimiters
|
||||
const contentEnd = cx.lineStart; // Start of the closing --- line
|
||||
|
||||
// Closing delimiter positions
|
||||
const closingMarkerStart = cx.lineStart;
|
||||
const closingMarkerEnd = cx.lineStart + line.text.length;
|
||||
|
||||
// Create marker elements for the --- delimiters
|
||||
const openingMarkerElem = cx.elt(frontMatterMarkerTagName, openingMarkerStart, openingMarkerEnd);
|
||||
const closingMarkerElem = cx.elt(frontMatterMarkerTagName, closingMarkerStart, closingMarkerEnd);
|
||||
|
||||
// Create the content element (the YAML content between delimiters)
|
||||
const contentElem = cx.elt(frontMatterContentTagName, contentStart, contentEnd);
|
||||
|
||||
// Create the container element spanning from start of first --- to end of last ---
|
||||
const containerElement = cx.elt(
|
||||
frontMatterTagName,
|
||||
0, // Start at document beginning
|
||||
closingMarkerEnd, // End after closing ---
|
||||
[openingMarkerElem, contentElem, closingMarkerElem],
|
||||
);
|
||||
if (foundEnd) {
|
||||
const closingMarkerStart = cx.lineStart;
|
||||
const closingMarkerEnd = cx.lineStart + line.text.length;
|
||||
const closingMarkerElem = cx.elt(frontMatterMarkerTagName, closingMarkerStart, closingMarkerEnd);
|
||||
|
||||
cx.addElement(containerElement);
|
||||
|
||||
// Move past the closing delimiter
|
||||
cx.nextLine();
|
||||
const containerElement = cx.elt(
|
||||
frontMatterTagName,
|
||||
0,
|
||||
closingMarkerEnd,
|
||||
[openingMarkerElem, contentElem, closingMarkerElem],
|
||||
);
|
||||
cx.addElement(containerElement);
|
||||
cx.nextLine();
|
||||
} else {
|
||||
const containerElement = cx.elt(
|
||||
frontMatterTagName,
|
||||
0,
|
||||
contentEnd,
|
||||
[openingMarkerElem, contentElem],
|
||||
);
|
||||
cx.addElement(containerElement);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
3
packages/editor/CodeMirror/theme.ts
vendored
3
packages/editor/CodeMirror/theme.ts
vendored
@@ -187,6 +187,9 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
'& .cm-strike': {
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
'& .cm-insert': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
|
||||
// Applying font size changes with CSS rather than the theme below works
|
||||
// around an issue where the border for code blocks in headings was too
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lezer/markdown": "1.6.3",
|
||||
"@replit/codemirror-vim": "6.2.1",
|
||||
"dompurify": "3.3.0",
|
||||
"dompurify": "3.3.1",
|
||||
"orderedmap": "2.1.1",
|
||||
"prosemirror-commands": "1.7.1",
|
||||
"prosemirror-dropcursor": "1.8.2",
|
||||
|
||||
@@ -4,6 +4,12 @@ describe('ArrayUtils', () => {
|
||||
|
||||
|
||||
|
||||
it('should return unique elements', (async () => {
|
||||
expect(ArrayUtils.unique(['un', 'deux', 'un', 'trois', 'deux'])).toEqual(['un', 'deux', 'trois']);
|
||||
expect(ArrayUtils.unique([1, 2, 1, 3, 2])).toEqual([1, 2, 3]);
|
||||
expect(ArrayUtils.unique([])).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should remove array elements', (async () => {
|
||||
let a = ['un', 'deux', 'trois'];
|
||||
a = ArrayUtils.removeElement(a, 'deux');
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const unique = function<T extends any>(array: T[]): T[] {
|
||||
return array.filter((elem, index, self) => {
|
||||
return index === self.indexOf(elem);
|
||||
});
|
||||
export const unique = function<T>(array: T[]): T[] {
|
||||
return [...new Set(array)];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const removeElement = function<T extends any>(array: T[], element: T): T[] {
|
||||
export const removeElement = function<T>(array: T[], element: T): T[] {
|
||||
const index = array.indexOf(element);
|
||||
if (index < 0) return array;
|
||||
const newArray = array.slice();
|
||||
@@ -15,8 +11,7 @@ export const removeElement = function<T extends any>(array: T[], element: T): T[
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/10264318/561309
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const binarySearch = function(items: any[], value: any) {
|
||||
export const binarySearch = function<T>(items: T[], value: T) {
|
||||
let startIndex = 0,
|
||||
stopIndex = items.length - 1,
|
||||
middle = Math.floor((stopIndex + startIndex) / 2);
|
||||
@@ -37,8 +32,7 @@ export const binarySearch = function(items: any[], value: any) {
|
||||
return items[middle] !== value ? -1 : middle;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const findByKey = function(array: any[], key: any, value: any) {
|
||||
export const findByKey = function<T, K extends keyof T>(array: T[], key: K, value: T[K]): T | null {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const o = array[i];
|
||||
if (typeof o !== 'object') continue;
|
||||
@@ -47,8 +41,7 @@ export const findByKey = function(array: any[], key: any, value: any) {
|
||||
return null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const contentEquals = function(array1: any[], array2: any[]) {
|
||||
export const contentEquals = function<T>(array1: T[], array2: T[]) {
|
||||
if (array1 === array2) return true;
|
||||
if (!array1.length && !array2.length) return true;
|
||||
if (array1.length !== array2.length) return false;
|
||||
@@ -85,8 +78,7 @@ export const mergeOverlappingIntervals = function(intervals: any[], limit: numbe
|
||||
return stack;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const shuffle = function(array: any[]) {
|
||||
export const shuffle = function<T>(array: T[]): T[] {
|
||||
array = array.slice();
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
@@ -98,13 +90,12 @@ export const shuffle = function(array: any[]) {
|
||||
};
|
||||
|
||||
// Used to replace lodash.pull, so that we don't need to import the whole
|
||||
// package. Not optimised.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const pull = (array: any[], ...elements: any[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const output: any[] = [];
|
||||
// package. Uses Set for O(N) performance.
|
||||
export const pull = <T>(array: T[], ...elements: T[]): T[] => {
|
||||
const removeSet = new Set(elements);
|
||||
const output: T[] = [];
|
||||
for (const e of array) {
|
||||
if (elements.includes(e)) continue;
|
||||
if (removeSet.has(e)) continue;
|
||||
output.push(e);
|
||||
}
|
||||
return output;
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ParseOptions {
|
||||
anchorNames?: string[];
|
||||
preserveImageTagsWithSize?: boolean;
|
||||
preserveNestedTables?: boolean;
|
||||
preserveTableStyles?: boolean;
|
||||
preserveColorStyles?: boolean;
|
||||
baseUrl?: string;
|
||||
disableEscapeContent?: boolean;
|
||||
@@ -26,6 +27,7 @@ export default class HtmlToMd {
|
||||
codeBlockStyle: 'fenced',
|
||||
preserveImageTagsWithSize: !!options.preserveImageTagsWithSize,
|
||||
preserveNestedTables: !!options.preserveNestedTables,
|
||||
preserveTableStyles: !!options.preserveTableStyles,
|
||||
preserveColorStyles: !!options.preserveColorStyles,
|
||||
bulletListMarker: '-',
|
||||
emDelimiter: '*',
|
||||
|
||||
@@ -233,6 +233,10 @@ const isListItem = function(line: string) {
|
||||
return line && line.trim().indexOf('- ') === 0;
|
||||
};
|
||||
|
||||
const isListContinuation = function(line: string) {
|
||||
return line && line.startsWith(' ') && !isListItem(line);
|
||||
};
|
||||
|
||||
const isCodeLine = function(line: string) {
|
||||
return line && line.indexOf('\t') === 0;
|
||||
};
|
||||
@@ -262,7 +266,7 @@ function formatMdLayout(lines: string[]) {
|
||||
const line = lines[i];
|
||||
|
||||
// Add a new line at the end of a list of items
|
||||
if (isListItem(previous) && line && !isListItem(line)) {
|
||||
if (isListItem(previous) && line && !isListItem(line) && !isListContinuation(line)) {
|
||||
newLines.push('');
|
||||
|
||||
// Add a new line at the beginning of a list of items
|
||||
@@ -897,7 +901,13 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: Extra
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
state.inPre = true;
|
||||
} else if (n === 'br') {
|
||||
section.lines.push(NEWLINE);
|
||||
if (state.lists.length) {
|
||||
const indent = ' '.repeat(state.lists.length - 1);
|
||||
section.lines.push(NEWLINE);
|
||||
section.lines.push(`${indent} `);
|
||||
} else {
|
||||
section.lines.push(NEWLINE);
|
||||
}
|
||||
} else if (n === 'en-media') {
|
||||
const hash = nodeAttributes.hash;
|
||||
|
||||
|
||||
@@ -60,6 +60,15 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
//
|
||||
// https://github.com/joplin/plugin-abc-sheet-music
|
||||
if (isInsideContainer(node, 'SVG')) return false;
|
||||
|
||||
// We exclude joplin-source because it contains the raw source
|
||||
// for editable blocks (mermaid diagrams, etc.). If we highlight
|
||||
// inside these elements, the <mark> tags corrupt the source code
|
||||
// and cause rendering to fail when switching editors.
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/14142
|
||||
if (node.parentElement?.closest('.joplin-source')) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
...extraOptions,
|
||||
|
||||
@@ -11,7 +11,7 @@ const emptyListRegex = /^(\s*)([*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s+)$/;
|
||||
export enum MarkdownTableJustify {
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
Right = 'right,',
|
||||
Right = 'right',
|
||||
}
|
||||
|
||||
export interface MarkdownTableHeader {
|
||||
|
||||
@@ -149,4 +149,47 @@ describe('stateToWhenClauseContext', () => {
|
||||
stateToWhenClauseContext(applicationState, { commandFolderIds }),
|
||||
).toHaveProperty('foldersAreDeleted', expectedDeletedState);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: 'should be false when no folders exist (new profile)',
|
||||
selectedFolderId: 'non-existent-folder',
|
||||
folders: [],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'should be false when selectedFolderId is null',
|
||||
selectedFolderId: null,
|
||||
folders: [{ id: '1', deleted_time: 0, share_id: '', parent_id: '' }],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'should be false when selected folder has been deleted',
|
||||
selectedFolderId: '1',
|
||||
folders: [{ id: '1', deleted_time: 1000, share_id: '', parent_id: '' }],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'should be true when selected folder exists and is not deleted',
|
||||
selectedFolderId: '1',
|
||||
folders: [{ id: '1', deleted_time: 0, share_id: '', parent_id: '' }],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
label: 'should be false when selectedFolderId does not match any folder',
|
||||
selectedFolderId: 'stale-id',
|
||||
folders: [{ id: '1', deleted_time: 0, share_id: '', parent_id: '' }],
|
||||
expected: false,
|
||||
},
|
||||
])('should set selectedFolderIsValid correctly: $label', ({ selectedFolderId, folders, expected }) => {
|
||||
const applicationState = buildState({
|
||||
selectedFolderId,
|
||||
folders,
|
||||
notes: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
stateToWhenClauseContext(applicationState),
|
||||
).toHaveProperty('selectedFolderIsValid', expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface WhenClauseContext {
|
||||
noteTodoCompleted: boolean;
|
||||
oneFolderSelected: boolean;
|
||||
oneNoteSelected: boolean;
|
||||
selectedFolderIsValid: boolean;
|
||||
someNotesSelected: boolean;
|
||||
syncStarted: boolean;
|
||||
hasActivePluginEditor: boolean;
|
||||
@@ -73,6 +74,14 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
|
||||
|
||||
const settings = state.settings || {};
|
||||
|
||||
// Check whether the window's selected folder actually exists and is not
|
||||
// deleted. This resolves the case where selectedFolderId is stale (e.g.
|
||||
// new profile with no notebooks) or points to a deleted folder.
|
||||
const selectedFolder = windowState.selectedFolderId
|
||||
? BaseModel.byId(state.folders, windowState.selectedFolderId)
|
||||
: null;
|
||||
const selectedFolderIsValid = !!selectedFolder && !selectedFolder.deleted_time;
|
||||
|
||||
return {
|
||||
// Application state
|
||||
notesAreBeingSaved: stateUtils.hasNotesBeingSaved(state),
|
||||
@@ -98,6 +107,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
|
||||
|
||||
// Folder selection
|
||||
oneFolderSelected: selectedFolderIds.length === 1,
|
||||
selectedFolderIsValid,
|
||||
|
||||
// Current note properties
|
||||
noteIsTodo: selectedNote ? !!selectedNote.is_todo : false,
|
||||
|
||||
@@ -323,6 +323,10 @@ describe('ShareService', () => {
|
||||
// in tests.
|
||||
const previousLogLevel = Logger.globalLogger.setLevel(LogLevel.Error);
|
||||
|
||||
// checkShareConsistency only runs when the sync target supports
|
||||
// sharing (Joplin Server/Cloud).
|
||||
Setting.setValue('sync.target', 9);
|
||||
|
||||
const service = testShareFolderService({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
'GET api/shares': async (_query: Record<string, any>, _body: any): Promise<any> => {
|
||||
@@ -340,6 +344,31 @@ describe('ShareService', () => {
|
||||
Logger.globalLogger.setLevel(previousLogLevel);
|
||||
});
|
||||
|
||||
it('should skip share consistency check when not using a share-compatible sync target', async () => {
|
||||
// Simulate a non-Joplin Server sync target (e.g. WebDAV = 6).
|
||||
// This reproduces the scenario where a user previously used Joplin
|
||||
// Server (which set share_id on folders), then switched to WebDAV.
|
||||
// checkShareConsistency() should not attempt to call the Joplin
|
||||
// Server API since we are no longer on a share-capable sync target.
|
||||
Setting.setValue('sync.target', 6);
|
||||
|
||||
const service = mockShareService({
|
||||
onExec: async () => {
|
||||
throw new Error('Should not call the API when share is not enabled');
|
||||
},
|
||||
});
|
||||
|
||||
// Create a folder with a stale share_id leftover from Joplin Server
|
||||
const folder = await Folder.save({ share_id: 'stale_share_id' });
|
||||
|
||||
// This should not throw or attempt any API calls
|
||||
await service.checkShareConsistency();
|
||||
|
||||
// The folder should still exist since we cannot verify shares
|
||||
// without a Joplin Server API connection
|
||||
expect(await Folder.load(folder.id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should leave a shared folder', async () => {
|
||||
const folder1 = await createFolderTree('', [
|
||||
{
|
||||
|
||||
@@ -247,6 +247,12 @@ export default class ShareService {
|
||||
// necessary otherwise sync will try to update items that are not longer
|
||||
// accessible and will throw the error "Could not find share with ID: xxxx")
|
||||
public async checkShareConsistency() {
|
||||
// Sharing is only supported on Joplin Server/Cloud. If the user is
|
||||
// using a different sync target, there is no share API to query and
|
||||
// any share_id values on folders are stale leftovers from a previous
|
||||
// sync target configuration.
|
||||
if (!this.enabled) return;
|
||||
|
||||
const rootSharedFolders = await Folder.rootSharedFolders(this.shares);
|
||||
let hasRefreshedShares = false;
|
||||
let shares = this.shares;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"jquery": "3.7.1",
|
||||
"knex": "3.1.0",
|
||||
"koa": "2.16.3",
|
||||
"ldapts": "8.0.14",
|
||||
"ldapts": "8.0.18",
|
||||
"markdown-it": "13.0.2",
|
||||
"mustache": "4.2.0",
|
||||
"node-cron": "3.0.3",
|
||||
|
||||
@@ -258,3 +258,5 @@ clearsign
|
||||
ligne
|
||||
payant
|
||||
llamacpp
|
||||
bgcolor
|
||||
bordercolor
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"v3.5.13": true,
|
||||
"v3.6.3": true,
|
||||
"android-v3.6.13": true,
|
||||
"ios-v13.6.2": true
|
||||
"ios-v13.6.2": true,
|
||||
"v3.6.4": true
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,8 @@ rules.table = {
|
||||
}
|
||||
}
|
||||
|
||||
const captionContent = node.caption ? node.caption.textContent || '' : '';
|
||||
const captionNode = node.querySelector ? node.querySelector('caption') : node.caption;
|
||||
const captionContent = captionNode ? captionNode.textContent || '' : '';
|
||||
const caption = captionContent ? `${captionContent}\n\n` : '';
|
||||
const tableContent = `${emptyHeader}${content}`.trimStart();
|
||||
return `\n\n${caption}${tableContent}\n\n`;
|
||||
@@ -209,6 +210,93 @@ const nodeContains = (node, types) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Style properties that count as user customization.
|
||||
// Excludes TinyMCE/Joplin defaults:
|
||||
// - border-collapse: set by default on all tables
|
||||
// - width: set on every cell by TinyMCE
|
||||
// - text-align: converted to Markdown alignment (:---, :---:, ---:)
|
||||
// - height: false positives from TinyMCE defaults
|
||||
const customStyleProperties = [
|
||||
'background-color', 'background',
|
||||
'border-color', 'border',
|
||||
'border-top', 'border-right', 'border-bottom', 'border-left',
|
||||
'border-style', 'border-width',
|
||||
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
||||
'float', 'margin-left', 'margin-right',
|
||||
];
|
||||
|
||||
// HTML attributes TinyMCE may set instead of CSS.
|
||||
const customAttributeNames = [
|
||||
'bgcolor',
|
||||
'bordercolor',
|
||||
'background',
|
||||
];
|
||||
|
||||
const nodeHasCustomStyle = (node) => {
|
||||
if (!node || !node.getAttribute) return false;
|
||||
const styleAttr = node.getAttribute('style');
|
||||
if (!styleAttr) return false;
|
||||
// Extract property names from the raw style string
|
||||
const properties = styleAttr.split(';')
|
||||
.map(s => s.split(':')[0].trim().toLowerCase())
|
||||
.filter(s => s.length > 0);
|
||||
for (let i = 0; i < properties.length; i++) {
|
||||
if (customStyleProperties.includes(properties[i])) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasNonDefaultSpacingAttribute = (node, name) => {
|
||||
if (!node || !node.getAttribute) return false;
|
||||
const value = node.getAttribute(name);
|
||||
if (value === null) return false;
|
||||
const normalisedValue = `${value}`.trim().toLowerCase();
|
||||
if (!normalisedValue) return false;
|
||||
if (normalisedValue === '0' || normalisedValue === '0px') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const nodeHasCustomAttributes = (node) => {
|
||||
if (!node || !node.getAttribute) return false;
|
||||
|
||||
for (let i = 0; i < customAttributeNames.length; i++) {
|
||||
const value = node.getAttribute(customAttributeNames[i]);
|
||||
if (value !== null && `${value}`.trim() !== '') return true;
|
||||
}
|
||||
|
||||
if (node.nodeName === 'TABLE') {
|
||||
if (hasNonDefaultSpacingAttribute(node, 'cellpadding')) return true;
|
||||
if (hasNonDefaultSpacingAttribute(node, 'cellspacing')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const nodeHasCustomFormatting = (node) => {
|
||||
return nodeHasCustomStyle(node) || nodeHasCustomAttributes(node);
|
||||
};
|
||||
|
||||
// Returns true if the table or any of its rows/cells have custom formatting.
|
||||
const tableHasCustomStyles = (tableNode) => {
|
||||
if (nodeHasCustomFormatting(tableNode)) return true;
|
||||
|
||||
const rows = tableNode.rows;
|
||||
if (!rows) return false;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
if (nodeHasCustomFormatting(row)) return true;
|
||||
for (let j = 0; j < row.childNodes.length; j++) {
|
||||
const cell = row.childNodes[j];
|
||||
if ((cell.nodeName === 'TD' || cell.nodeName === 'TH') && nodeHasCustomFormatting(cell)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const tableShouldBeHtml = (tableNode, options) => {
|
||||
const possibleTags = [
|
||||
'UL',
|
||||
@@ -231,7 +319,8 @@ const tableShouldBeHtml = (tableNode, options) => {
|
||||
if (options.preserveNestedTables) possibleTags.push('TABLE');
|
||||
|
||||
return nodeContains(tableNode, 'code') ||
|
||||
nodeContains(tableNode, possibleTags);
|
||||
nodeContains(tableNode, possibleTags) ||
|
||||
(options.preserveTableStyles && tableHasCustomStyles(tableNode));
|
||||
}
|
||||
|
||||
// Various conditions under which a table should be skipped - i.e. each cell
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function taskListItems (turndownService) {
|
||||
const parent = node.parentNode;
|
||||
const grandparent = parent.parentNode;
|
||||
const grandparentIsListItem = !!grandparent && grandparent.nodeName === 'LI';
|
||||
return (node.type === 'checkbox' || node.role === 'checkbox') && (
|
||||
return (node.type === 'checkbox' || node.getAttribute('role') === 'checkbox') && (
|
||||
parent.nodeName === 'LI'
|
||||
// Handles the case where the label contains the checkbox. For example,
|
||||
// <label><input ...> ...label text...</label>
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function(config) {
|
||||
name: 'TurndownService',
|
||||
...config.output,
|
||||
},
|
||||
external: ['jsdom'],
|
||||
external: ['@mixmark-io/domino'],
|
||||
plugins: [
|
||||
commonjs(),
|
||||
replace({
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"browser": "lib/turndown.browser.cjs.js",
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "4.4.4",
|
||||
"html-entities": "1.4.0",
|
||||
"jsdom": "26.1.0"
|
||||
"@mixmark-io/domino": "2.2.0",
|
||||
"html-entities": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "28.0.9",
|
||||
|
||||
@@ -47,9 +47,9 @@ function createHTMLParser () {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var JSDOM = require('jsdom').JSDOM
|
||||
var domino = require('@mixmark-io/domino')
|
||||
Parser.prototype.parseFromString = function (string) {
|
||||
return new JSDOM(string).window.document
|
||||
return domino.createDocument(string)
|
||||
}
|
||||
}
|
||||
return Parser
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# Joplin Desktop Changelog
|
||||
|
||||
## [v3.6.4](https://github.com/laurent22/joplin/releases/tag/v3.6.4) (Pre-release) - 2026-03-07T17:19:55Z
|
||||
|
||||
- New: Add "Paste as Markdown" command for Markdown editor ([#14556](https://github.com/laurent22/joplin/issues/14556))
|
||||
- New: Plugins: Add support for `joplin.fs.archiveExtract` plugin method ([#14625](https://github.com/laurent22/joplin/issues/14625))
|
||||
- Improved: Add keyboard shortcuts to toolbar buttons ([#14408](https://github.com/laurent22/joplin/issues/14408)) ([#12326](https://github.com/laurent22/joplin/issues/12326) by [@akshajrawat](https://github.com/akshajrawat))
|
||||
- Improved: Show feedback message when master passwords do not match ([#14566](https://github.com/laurent22/joplin/issues/14566) by [@Vinayreddy765](https://github.com/Vinayreddy765))
|
||||
- Improved: Translate Find and Replace dialog in Rich Text editor ([#14529](https://github.com/laurent22/joplin/issues/14529)) ([#12210](https://github.com/laurent22/joplin/issues/12210) by [@Harsh16gupta](https://github.com/Harsh16gupta))
|
||||
- Fixed: App fails to restart on Linux AppImage ([#14530](https://github.com/laurent22/joplin/issues/14530)) ([#14522](https://github.com/laurent22/joplin/issues/14522) by [@Ahmed-Idani](https://github.com/Ahmed-Idani))
|
||||
- Fixed: Auto-scroll to selected note from 'Go to Anything' search results ([#14591](https://github.com/laurent22/joplin/issues/14591)) ([#12355](https://github.com/laurent22/joplin/issues/12355) by [@Harsh16gupta](https://github.com/Harsh16gupta))
|
||||
- Fixed: Error message is incorrect when plugin manifest is invalid ([#14374](https://github.com/laurent22/joplin/issues/14374)) ([#14271](https://github.com/laurent22/joplin/issues/14271) by [@akshajrawat](https://github.com/akshajrawat))
|
||||
- Fixed: Fix ++insert++ syntax rendering fix in markdown ([#14547](https://github.com/laurent22/joplin/issues/14547)) ([#14543](https://github.com/laurent22/joplin/issues/14543) by [@justin212407](https://github.com/justin212407))
|
||||
- Fixed: Fix file:// links with backslashes for Windows UNC paths ([#14541](https://github.com/laurent22/joplin/issues/14541)) ([#14196](https://github.com/laurent22/joplin/issues/14196) by Sriram Varun Kumar)
|
||||
- Fixed: Fix issue where the revision service does not start on the first launch of the app ([#14554](https://github.com/laurent22/joplin/issues/14554) by [@mrjo118](https://github.com/mrjo118))
|
||||
- Fixed: Fixes zh_TW locale detection on first start ([#14527](https://github.com/laurent22/joplin/issues/14527)) ([#14500](https://github.com/laurent22/joplin/issues/14500) by Ashutosh Singh)
|
||||
- Fixed: Implement cursor-aware markup rendering and hide bulletpoints on task lists ([#14573](https://github.com/laurent22/joplin/issues/14573)) ([#14564](https://github.com/laurent22/joplin/issues/14564) by [@bwat47](https://github.com/bwat47))
|
||||
- Fixed: Importing from OneNote: Fix importing cross-page links ([#14567](https://github.com/laurent22/joplin/issues/14567) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Invisible cursor in legacy editor when using dark theme in separate window ([#14557](https://github.com/laurent22/joplin/issues/14557)) ([#13178](https://github.com/laurent22/joplin/issues/13178) by [@yugalkaushik](https://github.com/yugalkaushik))
|
||||
- Fixed: Normalize img alt line breaks and convert data: URLs when pasting from Word ([#14518](https://github.com/laurent22/joplin/issues/14518)) ([#13140](https://github.com/laurent22/joplin/issues/13140) by [@manjhss](https://github.com/manjhss))
|
||||
- Fixed: Prevent All Notes sort order from overwriting shared notebook sort on relaunch ([#14524](https://github.com/laurent22/joplin/issues/14524)) ([#12313](https://github.com/laurent22/joplin/issues/12313) by [@manjhss](https://github.com/manjhss))
|
||||
- Fixed: Prevent a failing plugin from blocking other plugins ([#14577](https://github.com/laurent22/joplin/issues/14577)) ([#12793](https://github.com/laurent22/joplin/issues/12793) by Ashutosh Singh)
|
||||
- Fixed: Secondary windows no longer follow primary selection after moving notes ([#14498](https://github.com/laurent22/joplin/issues/14498)) ([#13883](https://github.com/laurent22/joplin/issues/13883) by [@parththirwani](https://github.com/parththirwani))
|
||||
- Fixed: Show only relevant options in context menu when right-clicking a note link ([#14528](https://github.com/laurent22/joplin/issues/14528)) ([#14525](https://github.com/laurent22/joplin/issues/14525) by [@manjhss](https://github.com/manjhss))
|
||||
|
||||
## [v3.6.3](https://github.com/laurent22/joplin/releases/tag/v3.6.3) (Pre-release) - 2026-03-02T18:21:09Z
|
||||
|
||||
- New: Add context menu to non-image resources in Markdown editor ([#14402](https://github.com/laurent22/joplin/issues/14402))
|
||||
|
||||
@@ -14,7 +14,7 @@ Joplin is "offline first", which means you always have all your data on your pho
|
||||
|
||||
The notes can be securely [synchronised](https://github.com/laurent22/joplin/blob/dev/readme/apps/sync/index.md) using [end-to-end encryption](https://github.com/laurent22/joplin/blob/dev/readme/apps/sync/e2ee.md) with various cloud services including Nextcloud, Dropbox, OneDrive and [Joplin Cloud](https://joplinapp.org/plans/).
|
||||
|
||||
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
|
||||
Full text search is available on all platforms to quickly find the information you need. The app can be customised using [plugins](https://joplinapp.org/plugins/) and themes, and you can also easily create your own.
|
||||
|
||||
The application is available for Windows, Linux, macOS, Android and iOS. A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/apps/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
|
||||
|
||||
|
||||
250
yarn.lock
250
yarn.lock
@@ -8682,184 +8682,184 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/aix-ppc64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/aix-ppc64@npm:0.25.12"
|
||||
"@esbuild/aix-ppc64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/aix-ppc64@npm:0.26.0"
|
||||
conditions: os=aix & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/android-arm64@npm:0.25.12"
|
||||
"@esbuild/android-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/android-arm64@npm:0.26.0"
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/android-arm@npm:0.25.12"
|
||||
"@esbuild/android-arm@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/android-arm@npm:0.26.0"
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/android-x64@npm:0.25.12"
|
||||
"@esbuild/android-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/android-x64@npm:0.26.0"
|
||||
conditions: os=android & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.25.12"
|
||||
"@esbuild/darwin-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.26.0"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/darwin-x64@npm:0.25.12"
|
||||
"@esbuild/darwin-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/darwin-x64@npm:0.26.0"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.25.12"
|
||||
"@esbuild/freebsd-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.26.0"
|
||||
conditions: os=freebsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.25.12"
|
||||
"@esbuild/freebsd-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.26.0"
|
||||
conditions: os=freebsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-arm64@npm:0.25.12"
|
||||
"@esbuild/linux-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-arm64@npm:0.26.0"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-arm@npm:0.25.12"
|
||||
"@esbuild/linux-arm@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-arm@npm:0.26.0"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ia32@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-ia32@npm:0.25.12"
|
||||
"@esbuild/linux-ia32@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-ia32@npm:0.26.0"
|
||||
conditions: os=linux & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-loong64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-loong64@npm:0.25.12"
|
||||
"@esbuild/linux-loong64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-loong64@npm:0.26.0"
|
||||
conditions: os=linux & cpu=loong64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-mips64el@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.25.12"
|
||||
"@esbuild/linux-mips64el@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.26.0"
|
||||
conditions: os=linux & cpu=mips64el
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ppc64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.25.12"
|
||||
"@esbuild/linux-ppc64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.26.0"
|
||||
conditions: os=linux & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-riscv64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.25.12"
|
||||
"@esbuild/linux-riscv64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.26.0"
|
||||
conditions: os=linux & cpu=riscv64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-s390x@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-s390x@npm:0.25.12"
|
||||
"@esbuild/linux-s390x@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-s390x@npm:0.26.0"
|
||||
conditions: os=linux & cpu=s390x
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/linux-x64@npm:0.25.12"
|
||||
"@esbuild/linux-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/linux-x64@npm:0.26.0"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/netbsd-arm64@npm:0.25.12"
|
||||
"@esbuild/netbsd-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/netbsd-arm64@npm:0.26.0"
|
||||
conditions: os=netbsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.25.12"
|
||||
"@esbuild/netbsd-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.26.0"
|
||||
conditions: os=netbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/openbsd-arm64@npm:0.25.12"
|
||||
"@esbuild/openbsd-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/openbsd-arm64@npm:0.26.0"
|
||||
conditions: os=openbsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.25.12"
|
||||
"@esbuild/openbsd-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.26.0"
|
||||
conditions: os=openbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openharmony-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/openharmony-arm64@npm:0.25.12"
|
||||
"@esbuild/openharmony-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/openharmony-arm64@npm:0.26.0"
|
||||
conditions: os=openharmony & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/sunos-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/sunos-x64@npm:0.25.12"
|
||||
"@esbuild/sunos-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/sunos-x64@npm:0.26.0"
|
||||
conditions: os=sunos & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-arm64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/win32-arm64@npm:0.25.12"
|
||||
"@esbuild/win32-arm64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/win32-arm64@npm:0.26.0"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-ia32@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/win32-ia32@npm:0.25.12"
|
||||
"@esbuild/win32-ia32@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/win32-ia32@npm:0.26.0"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-x64@npm:0.25.12":
|
||||
version: 0.25.12
|
||||
resolution: "@esbuild/win32-x64@npm:0.25.12"
|
||||
"@esbuild/win32-x64@npm:0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@esbuild/win32-x64@npm:0.26.0"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -10539,7 +10539,7 @@ __metadata:
|
||||
electron-builder: "npm:24.13.3"
|
||||
electron-updater: "npm:6.6.8"
|
||||
electron-window-state: "npm:5.0.3"
|
||||
esbuild: "npm:^0.25.3"
|
||||
esbuild: "npm:^0.26.0"
|
||||
formatcoords: "npm:1.1.3"
|
||||
fs-extra: "npm:11.3.2"
|
||||
glob: "npm:11.0.3"
|
||||
@@ -10642,7 +10642,7 @@ __metadata:
|
||||
constants-browserify: "npm:1.0.0"
|
||||
crypto-browserify: "npm:3.12.1"
|
||||
deprecated-react-native-prop-types: "npm:5.0.0"
|
||||
esbuild: "npm:0.25.12"
|
||||
esbuild: "npm:0.26.0"
|
||||
events: "npm:3.3.0"
|
||||
expo: "npm:54.0.31"
|
||||
expo-av: "npm:16.0.8"
|
||||
@@ -10780,7 +10780,7 @@ __metadata:
|
||||
"@types/react": "npm:19.1.10"
|
||||
"@types/react-redux": "npm:7.1.33"
|
||||
"@types/styled-components": "npm:5.1.32"
|
||||
dompurify: "npm:3.3.0"
|
||||
dompurify: "npm:3.3.1"
|
||||
jest: "npm:29.7.0"
|
||||
jest-environment-jsdom: "npm:29.7.0"
|
||||
orderedmap: "npm:2.1.1"
|
||||
@@ -11132,7 +11132,7 @@ __metadata:
|
||||
jsdom: "npm:26.1.0"
|
||||
knex: "npm:3.1.0"
|
||||
koa: "npm:2.16.3"
|
||||
ldapts: "npm:8.0.14"
|
||||
ldapts: "npm:8.0.18"
|
||||
markdown-it: "npm:13.0.2"
|
||||
mustache: "npm:4.2.0"
|
||||
node-cron: "npm:3.0.3"
|
||||
@@ -11256,12 +11256,12 @@ __metadata:
|
||||
resolution: "@joplin/turndown@workspace:packages/turndown"
|
||||
dependencies:
|
||||
"@adobe/css-tools": "npm:4.4.4"
|
||||
"@mixmark-io/domino": "npm:2.2.0"
|
||||
"@rollup/plugin-commonjs": "npm:28.0.9"
|
||||
"@rollup/plugin-node-resolve": "npm:16.0.3"
|
||||
"@rollup/plugin-replace": "npm:6.0.3"
|
||||
browserify: "npm:14.5.0"
|
||||
html-entities: "npm:1.4.0"
|
||||
jsdom: "npm:26.1.0"
|
||||
rollup: "npm:4.2.0"
|
||||
standard: "npm:17.1.2"
|
||||
turndown-attendant: "npm:0.0.3"
|
||||
@@ -12639,7 +12639,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mixmark-io/domino@npm:^2.2.0":
|
||||
"@mixmark-io/domino@npm:2.2.0, @mixmark-io/domino@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "@mixmark-io/domino@npm:2.2.0"
|
||||
checksum: 10/839624ba6baab655c4f7393e8b8561516849926651e02f40484729b9869436b1e077906810bcac0bba4762448512d3ebd2f6d9b463d8ab0d5f54d75ca5306519
|
||||
@@ -26289,7 +26289,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dompurify@npm:3.3.0, dompurify@npm:^3.2.5":
|
||||
"dompurify@npm:3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "dompurify@npm:3.3.1"
|
||||
dependencies:
|
||||
"@types/trusted-types": "npm:^2.0.7"
|
||||
dependenciesMeta:
|
||||
"@types/trusted-types":
|
||||
optional: true
|
||||
checksum: 10/f71cca489e628591165d16e8cf4fa4f0d3e2ee48db4d73e9d2c5bedc6f915c92f9e9f101f8c4ba790bec0cdffe7f4e1747f5e31c69dc53ce7ae20a81ff6b0022
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dompurify@npm:^3.2.5":
|
||||
version: 3.3.0
|
||||
resolution: "dompurify@npm:3.3.0"
|
||||
dependencies:
|
||||
@@ -27436,36 +27448,36 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:0.25.12, esbuild@npm:^0.25.3":
|
||||
version: 0.25.12
|
||||
resolution: "esbuild@npm:0.25.12"
|
||||
"esbuild@npm:0.26.0, esbuild@npm:^0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "esbuild@npm:0.26.0"
|
||||
dependencies:
|
||||
"@esbuild/aix-ppc64": "npm:0.25.12"
|
||||
"@esbuild/android-arm": "npm:0.25.12"
|
||||
"@esbuild/android-arm64": "npm:0.25.12"
|
||||
"@esbuild/android-x64": "npm:0.25.12"
|
||||
"@esbuild/darwin-arm64": "npm:0.25.12"
|
||||
"@esbuild/darwin-x64": "npm:0.25.12"
|
||||
"@esbuild/freebsd-arm64": "npm:0.25.12"
|
||||
"@esbuild/freebsd-x64": "npm:0.25.12"
|
||||
"@esbuild/linux-arm": "npm:0.25.12"
|
||||
"@esbuild/linux-arm64": "npm:0.25.12"
|
||||
"@esbuild/linux-ia32": "npm:0.25.12"
|
||||
"@esbuild/linux-loong64": "npm:0.25.12"
|
||||
"@esbuild/linux-mips64el": "npm:0.25.12"
|
||||
"@esbuild/linux-ppc64": "npm:0.25.12"
|
||||
"@esbuild/linux-riscv64": "npm:0.25.12"
|
||||
"@esbuild/linux-s390x": "npm:0.25.12"
|
||||
"@esbuild/linux-x64": "npm:0.25.12"
|
||||
"@esbuild/netbsd-arm64": "npm:0.25.12"
|
||||
"@esbuild/netbsd-x64": "npm:0.25.12"
|
||||
"@esbuild/openbsd-arm64": "npm:0.25.12"
|
||||
"@esbuild/openbsd-x64": "npm:0.25.12"
|
||||
"@esbuild/openharmony-arm64": "npm:0.25.12"
|
||||
"@esbuild/sunos-x64": "npm:0.25.12"
|
||||
"@esbuild/win32-arm64": "npm:0.25.12"
|
||||
"@esbuild/win32-ia32": "npm:0.25.12"
|
||||
"@esbuild/win32-x64": "npm:0.25.12"
|
||||
"@esbuild/aix-ppc64": "npm:0.26.0"
|
||||
"@esbuild/android-arm": "npm:0.26.0"
|
||||
"@esbuild/android-arm64": "npm:0.26.0"
|
||||
"@esbuild/android-x64": "npm:0.26.0"
|
||||
"@esbuild/darwin-arm64": "npm:0.26.0"
|
||||
"@esbuild/darwin-x64": "npm:0.26.0"
|
||||
"@esbuild/freebsd-arm64": "npm:0.26.0"
|
||||
"@esbuild/freebsd-x64": "npm:0.26.0"
|
||||
"@esbuild/linux-arm": "npm:0.26.0"
|
||||
"@esbuild/linux-arm64": "npm:0.26.0"
|
||||
"@esbuild/linux-ia32": "npm:0.26.0"
|
||||
"@esbuild/linux-loong64": "npm:0.26.0"
|
||||
"@esbuild/linux-mips64el": "npm:0.26.0"
|
||||
"@esbuild/linux-ppc64": "npm:0.26.0"
|
||||
"@esbuild/linux-riscv64": "npm:0.26.0"
|
||||
"@esbuild/linux-s390x": "npm:0.26.0"
|
||||
"@esbuild/linux-x64": "npm:0.26.0"
|
||||
"@esbuild/netbsd-arm64": "npm:0.26.0"
|
||||
"@esbuild/netbsd-x64": "npm:0.26.0"
|
||||
"@esbuild/openbsd-arm64": "npm:0.26.0"
|
||||
"@esbuild/openbsd-x64": "npm:0.26.0"
|
||||
"@esbuild/openharmony-arm64": "npm:0.26.0"
|
||||
"@esbuild/sunos-x64": "npm:0.26.0"
|
||||
"@esbuild/win32-arm64": "npm:0.26.0"
|
||||
"@esbuild/win32-ia32": "npm:0.26.0"
|
||||
"@esbuild/win32-x64": "npm:0.26.0"
|
||||
dependenciesMeta:
|
||||
"@esbuild/aix-ppc64":
|
||||
optional: true
|
||||
@@ -27521,7 +27533,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
esbuild: bin/esbuild
|
||||
checksum: 10/bc9c03d64e96a0632a926662c9d29decafb13a40e5c91790f632f02939bc568edc9abe0ee5d8055085a2819a00139eb12e223cfb8126dbf89bbc569f125d91fd
|
||||
checksum: 10/c74cf3adb9e19b877ac2e4f52623fd83aeef5759b005703704f32139b0187cd7713e8e9e17e14caae8d4e67619ca8d7ce9014732866fb9bad49ee206f1f5e134
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -36189,16 +36201,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ldapts@npm:8.0.14":
|
||||
version: 8.0.14
|
||||
resolution: "ldapts@npm:8.0.14"
|
||||
"ldapts@npm:8.0.18":
|
||||
version: 8.0.18
|
||||
resolution: "ldapts@npm:8.0.18"
|
||||
dependencies:
|
||||
"@types/asn1": "npm:>=0.2.4"
|
||||
asn1: "npm:0.2.6"
|
||||
debug: "npm:4.4.3"
|
||||
strict-event-emitter-types: "npm:2.0.0"
|
||||
whatwg-url: "npm:15.1.0"
|
||||
checksum: 10/0f3856cf36139be2b094f80c0e0b68f99474d859d71c6fae791ea747310d32f968bf010dcd27dd55ce29cb5a7505b059a1628f37d673b769d99c39853fb49604
|
||||
checksum: 10/bc3f3a0c3bd28c51520dc1337cab5d654806a84103402976dc187433f7840a003a23b10fbb74035fbdb48200cc00c77649ef229155cf43f11bc98727455c8e5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user