1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-11 00:21:45 +02:00

Compare commits

..

29 Commits

Author SHA1 Message Date
Laurent Cozic
e64f141b28 Android 3.5.7 2026-01-08 19:42:31 +00:00
Laurent Cozic
8bba68d920 Chore: Katex build files 2026-01-08 19:42:23 +00:00
Laurent Cozic
e342f2d572 Desktop release v3.5.10 2026-01-08 19:21:20 +00:00
Henry Heino
5951a66fef Desktop, Mobile: Resolves #13753: Markdown editor: Make header styles more closely match the note viewer (#14053) 2026-01-08 09:24:00 +00:00
Henry Heino
04f9bda128 Desktop: OneNote import: Fix all imported notes have the language marked as "English" (#14054) 2026-01-08 09:23:35 +00:00
Henry Heino
7a8a94f557 Mobile: Rich Text Editor: Add shortcuts for inserting code blocks (#14055) 2026-01-08 09:22:19 +00:00
Henry Heino
ad000fb521 Desktop,Mobile: Fixes #14049: Fix ABC Sheet Music setting includes "Translation error" in description (#14058) 2026-01-08 09:21:55 +00:00
Henry Heino
435b896142 Desktop, Mobile: Accessibility: In-editor rendering: Fix rendered checkboxes are very small on mobile (#14056) 2026-01-08 09:19:56 +00:00
renovate[bot]
b12f31c802 Update dependency katex to v0.16.23 (#14018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 16:43:31 +00:00
Laurent Cozic
ddb6d7a677 Desktop: Fixes #14040: Rich Text Editor: ABC sheet music options lost on edit 2026-01-07 12:14:54 +00:00
AlterWill
f0a1d05284 Chore: Fixes #13629: Fix focusHandler warning when navigating (#13973) 2026-01-07 11:56:45 +00:00
Alejandro Saucedo
27f7cb7ca6 Cli: Added keymap command to print existing keybinds in CLI and TUI (#13984)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-01-07 11:56:06 +00:00
Alejandro Saucedo
9e43ebcf43 Chore: Fixes #13983: Remove conflicting macos dependency for devbox (#13985) 2026-01-07 11:54:33 +00:00
Ahmed Idani
05cc0fa798 Desktop, Mobile: Fixes #13229: Insert time command not respecting locale settings (#13994) 2026-01-07 11:48:15 +00:00
Henry Heino
ee5b631d13 Desktop: Built-in plugins: Update Freehand Drawing to v4.2.0 (#14002) 2026-01-07 11:47:46 +00:00
Henry Heino
e4b6b34d37 Desktop: Built-in plugins: Update Backup to v1.5.1 (#14003) 2026-01-07 11:47:38 +00:00
Gerd Naschenweng
6f1280f0f5 Doc: Add Mailbox.org WebDAV to sync options (#14016) 2026-01-07 11:44:03 +00:00
bwat47
4c9015dab4 Desktop, Mobile: Fixes #13963: Images sometimes don't render until you click somewhere in the note (#14019) 2026-01-07 11:39:58 +00:00
Henry Heino
1adcafce9d Desktop, Mobile: Fixes #14030: Fix "Check synchronization configuration" button (#14031) 2026-01-07 11:37:35 +00:00
Henry Heino
cc9f55e115 Chore: Refactoring: Improve ObjectUtils types (#14032) 2026-01-07 11:36:43 +00:00
Henry Heino
e8b3b039df Desktop: Accessibility: Make sidebar "jump to next match" case insensitive (#14033) 2026-01-07 11:36:30 +00:00
Henry Heino
d9295a69d1 Chore: OneNote importer: Don't require IS_CONTINUOUS_INTEGRATION for a dev build (#14034) 2026-01-07 11:36:11 +00:00
Henry Heino
b92743b068 Desktop: Resolves #14004: OneNote import: Improve ID resolution (#14035) 2026-01-07 11:35:53 +00:00
Henry Heino
03f65a3fb1 Windows: Fixes #13549: Importing from OneNote: Fix badly encoded accents in notebook titles (#14037) 2026-01-07 11:35:42 +00:00
Henry Heino
32a22174f7 Desktop, Mobile: Resolves #13159: Markdown editor: Prevent layout shift when hiding/showing rendered checkboxes (#14044) 2026-01-07 11:34:51 +00:00
Henry Heino
d154ef4f5c Chore: Desktop: Fix "net::ERR_FILE_NOT_FOUND" logged to stdout when an invalid resource is requested from the note viewer (#14045) 2026-01-07 11:34:40 +00:00
Henry Heino
b8dd660c28 Desktop: OneNote import: Fix video embeds aren't imported: Import video embeds as links (#14046) 2026-01-07 11:34:31 +00:00
Henry Heino
2b20315bf5 Desktop: OneNote import: Simplify imported HTML (#14047) 2026-01-07 11:34:23 +00:00
nickprotop
93b9108832 All: Translation: Update el_GR.po (#14036) 2026-01-06 19:20:52 -05:00
61 changed files with 1208 additions and 916 deletions

View File

@@ -115,6 +115,7 @@ packages/app-cli/app/command-export.js
packages/app-cli/app/command-geoloc.js
packages/app-cli/app/command-help.js
packages/app-cli/app/command-import.js
packages/app-cli/app/command-keymap.js
packages/app-cli/app/command-ls.js
packages/app-cli/app/command-mkbook.test.js
packages/app-cli/app/command-mkbook.js
@@ -1227,6 +1228,7 @@ packages/lib/InMemoryCache.js
packages/lib/JoplinDatabase.js
packages/lib/JoplinError.js
packages/lib/JoplinServerApi.js
packages/lib/ObjectUtils.test.js
packages/lib/ObjectUtils.js
packages/lib/PerformanceLogger.test.js
packages/lib/PerformanceLogger.js

2
.gitignore vendored
View File

@@ -88,6 +88,7 @@ packages/app-cli/app/command-export.js
packages/app-cli/app/command-geoloc.js
packages/app-cli/app/command-help.js
packages/app-cli/app/command-import.js
packages/app-cli/app/command-keymap.js
packages/app-cli/app/command-ls.js
packages/app-cli/app/command-mkbook.test.js
packages/app-cli/app/command-mkbook.js
@@ -1200,6 +1201,7 @@ packages/lib/InMemoryCache.js
packages/lib/JoplinDatabase.js
packages/lib/JoplinError.js
packages/lib/JoplinServerApi.js
packages/lib/ObjectUtils.test.js
packages/lib/ObjectUtils.js
packages/lib/PerformanceLogger.test.js
packages/lib/PerformanceLogger.js

View File

@@ -11,11 +11,6 @@
},
"nodejs": "24.5.0",
"pkg-config": "latest",
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
"version": "",
"platforms": ["aarch64-darwin", "x86_64-darwin"],
},
"python": "3.13.3",
"bat": "latest",
"electron": {

View File

@@ -0,0 +1,48 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
const { cliUtils } = require('./cli-utils.js');
interface Args { }
class Command extends BaseCommand {
public override usage() {
return 'keymap';
}
public override description() {
return _('Displays the configured keyboard shortcuts.');
}
public override compatibleUis() {
return ['cli', 'gui'];
}
public override async action(_args: Args) {
const keymaps = await app().loadKeymaps();
this.stdout(_('Configured keyboard shortcuts:\n'));
const rows = [];
const padding = ' ';
rows.push([`${padding}${_('KEYS')}`, _('TYPE'), _('COMMAND')]);
rows.push([`${padding}----`, '----', '-------']);
for (const item of keymaps) {
const formattedKeys = item.keys
.map((k: string) => (k === ' ' ? `(${'SPACE'})` : k))
.join(', ');
rows.push([padding + formattedKeys, item.type, item.command]);
}
cliUtils.printArray(this.stdout.bind(this), rows);
if (app().gui() && !app().gui().isDummy()) {
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
}
module.exports = Command;

View File

@@ -0,0 +1,10 @@
<div class="joplin-editable joplin-abc-notation">
<pre class="joplin-source" data-abc-options="{&quot;responsive&quot;:&quot;resize&quot;}" data-joplin-language="abc" data-joplin-source-open="```abc&#10;" data-joplin-source-close="&#10;```&#10;">{responsive:'resize'}
---
K:F
!f!(fgag-g2c2)|</pre>
<pre class="joplin-rendered joplin-abc-notation-rendered">K:F
!f!(fgag-g2c2)|</pre>
</div>

View File

@@ -0,0 +1,6 @@
```abc
{ responsive: 'resize' }
---
K:F
!f!(fgag-g2c2)|
```

View File

@@ -2,7 +2,7 @@ import { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
import { WindowCommandDependencies, NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext, RegisteredRuntime } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
import { formatMsToLocal } from '@joplin/utils/time';
import { reg } from '@joplin/lib/registry';
import getWindowCommandPriority from './getWindowCommandPriority';
@@ -50,7 +50,7 @@ function editorCommandRuntime(
if (declaration.name === 'insertDateTime') {
return editorRef.current.execCommand({
name: 'insertText',
value: time.formatMsToLocal(new Date().getTime()),
value: formatMsToLocal(Date.now()),
});
} else if (declaration.name === 'scrollToHash') {
return editorRef.current.scrollTo({

View File

@@ -51,9 +51,11 @@ const getParentOffset = (childIndex: number, listItems: ListItem[]): number|null
};
const findNextTypeAheadMatch = (selectedIndex: number, query: string, listItems: ListItem[]) => {
const normalize = (text: string) => text.trim().toLowerCase();
const matches = (item: ListItem) => {
return item.label.startsWith(query);
return normalize(item.label).startsWith(normalize(query));
};
const indexBefore = listItems.slice(0, selectedIndex).findIndex(matches);
// Search in all results **after** the current. This prevents the current item from
// always being identified as the next match, if the user repeatedly presses the

View File

@@ -123,8 +123,8 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
};
const tabIndex = indexInFocusable === (selectedIndex % focusableItems.length) ? 0 : -1;
const setButtonRefCallback = (button: HTMLButtonElement) => {
if (tabIndex === 0 && containerHasFocus) {
const setButtonRefCallback = (button: HTMLButtonElement | null) => {
if (button && tabIndex === 0 && containerHasFocus) {
focus('ToolbarBase', button);
}
};

View File

@@ -64,6 +64,10 @@ test.describe('sidebar', () => {
await expect(mainWindow.locator(':focus')).toHaveText('Folder b');
await mainWindow.keyboard.type('A');
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
// Should be case-insensitive
await mainWindow.keyboard.type('f');
await expect(mainWindow.locator(':focus')).toHaveText('Folder b');
});
test('left/right arrow keys should expand/collapse notebooks', async ({ electronApp, mainWindow }) => {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.5.9",
"version": "3.5.10",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,

View File

@@ -130,6 +130,12 @@ const makeAccessDeniedResponse = (message: string) => {
});
};
const makeNotFoundResponse = () => {
return new Response('not found', {
status: 404,
});
};
// Creating a custom protocol allows us to isolate iframes by giving them
// different domain names from the main Joplin app.
//
@@ -210,10 +216,24 @@ const handleCustomProtocols = (): CustomProtocolHandler => {
const rangeHeader = request.headers.get('Range');
let response;
if (!rangeHeader) {
response = await net.fetch(asFileUrl);
} else {
response = await handleRangeRequest(request, pathname);
try {
if (!rangeHeader) {
response = await net.fetch(asFileUrl);
} else {
response = await handleRangeRequest(request, pathname);
}
} catch (error) {
if (
// Errors from NodeJS fs methods (e.g. fs.stat()
error.code === 'ENOENT'
// Errors from Electron's net.fetch(). Use error.message since these errors don't
// seem to have a specific .code or .name.
|| error.message === 'net::ERR_FILE_NOT_FOUND'
) {
response = makeNotFoundResponse();
} else {
throw error;
}
}
if (mediaOnly) {

View File

@@ -89,8 +89,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097786
versionName "3.5.6"
versionCode 2097787
versionName "3.5.7"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -1,7 +1,7 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { CommandRuntimeProps } from '../types';
import time from '@joplin/lib/time';
import { formatMsToLocal } from '@joplin/utils/time';
export const declaration: CommandDeclaration = {
name: 'insertDateTime',
@@ -12,7 +12,7 @@ export const declaration: CommandDeclaration = {
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
props.insertText(time.formatDateToLocal(new Date()));
props.insertText(formatMsToLocal(Date.now()));
},
enabledCondition: '!noteIsReadOnly',

View File

@@ -1,5 +1,5 @@
module.exports = {
hash:"daebd8498ff273c64cf5905d4356e66a", files: {
hash:"88e5d809af57eac7b86c4deaf21b9345", files: {
'abc/abc_render.js': { data: require('./abc/abc_render.js.base64.js'), mime: 'application/javascript', encoding: 'base64' },
'abc/abcjs-basic-min.js': { data: require('./abc/abcjs-basic-min.js.base64.js'), mime: 'application/javascript', encoding: 'base64' },
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },

View File

@@ -1 +1 @@
module.exports = {"hash":"daebd8498ff273c64cf5905d4356e66a","files":["abc/abc_render.js","abc/abcjs-basic-min.js","highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
module.exports = {"hash":"88e5d809af57eac7b86c4deaf21b9345","files":["abc/abc_render.js","abc/abcjs-basic-min.js","highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}

File diff suppressed because one or more lines are too long

View File

@@ -7,115 +7,129 @@ import { tmpdir } from 'os';
import { chdir, cwd } from 'process';
import { execCommand } from '@joplin/utils';
import { glob } from 'glob';
import readRepositoryJson from './utils/readRepositoryJson';
import readRepositoryJson, { BuiltInPluginType, RepositoryData } from './utils/readRepositoryJson';
import getPathToPatchFileFor from './utils/getPathToPatchFileFor';
import getCurrentCommitHash from './utils/getCurrentCommitHash';
import { waitForCliInput } from '@joplin/utils/cli';
interface Options {
outputParentDir: string|null;
beforeInstall: (buildDir: string, pluginName: string)=> Promise<void>;
beforePatch: ()=> Promise<void>;
}
const buildDefaultPlugins = async (outputParentDir: string|null, options: Options) => {
const pluginSourcesDir = resolve(join(__dirname, 'plugin-sources'));
const buildDefaultPlugins = async (options: Options) => {
const pluginRepositoryData = await readRepositoryJson(join(__dirname, 'pluginRepositories.json'));
const originalDirectory = cwd();
const logStatus = (...message: string[]) => {
const blue = '\x1b[96m';
const reset = '\x1b[0m';
console.log(blue, ...message, reset);
};
for (const pluginId in pluginRepositoryData) {
const repositoryData = pluginRepositoryData[pluginId];
const outputPath = options.outputParentDir && join(options.outputParentDir, `${pluginId}.jpl`);
const buildDir = await mkdtemp(join(tmpdir(), 'default-plugin-build'));
try {
logStatus('Building plugin', pluginId, 'at', buildDir);
const pluginDir = resolve(join(pluginSourcesDir, pluginId));
// Clone the repository if not done yet
if (!(await exists(pluginDir)) || (await readdir(pluginDir)).length === 0) {
logStatus(`Cloning from repository ${repositoryData.cloneUrl}`);
await execCommand(['git', 'clone', '--', repositoryData.cloneUrl, pluginDir]);
chdir(pluginDir);
if (repositoryData.type === BuiltInPluginType.Built) {
await buildPlugin(pluginId, repositoryData, outputPath, options);
} else {
if (!outputPath) {
console.warn('Skipping NPM plugin,', pluginId, ': missing output path.');
continue;
}
chdir(pluginDir);
const expectedCommitHash = repositoryData.commit;
logStatus(`Switching to commit ${expectedCommitHash}`);
await execCommand(['git', 'switch', repositoryData.branch]);
try {
await execCommand(['git', 'checkout', expectedCommitHash]);
} catch (error) {
logStatus(`git checkout failed with error ${error}. Fetching...`);
await execCommand(['git', 'fetch']);
await execCommand(['git', 'checkout', expectedCommitHash]);
}
if (await getCurrentCommitHash() !== expectedCommitHash) {
throw new Error(`Unable to checkout commit ${expectedCommitHash}`);
}
logStatus('Copying repository files...');
await copy(pluginDir, buildDir, {
filter: fileName => {
return basename(fileName) !== '.git';
},
});
chdir(buildDir);
logStatus('Initializing repository.');
await execCommand('git init . -b main');
logStatus('Running before-patch hook.');
await options.beforePatch();
const patchFile = getPathToPatchFileFor(pluginId);
if (await exists(patchFile)) {
logStatus('Applying patch.');
await execCommand(['git', 'apply', patchFile]);
}
await options.beforeInstall(buildDir, pluginId);
logStatus('Installing dependencies.');
await execCommand('npm install');
const jplFiles = await glob('publish/*.jpl');
logStatus(`Found built .jpl files: ${JSON.stringify(jplFiles)}`);
if (jplFiles.length === 0) {
throw new Error(`No published files found in ${buildDir}/publish`);
}
if (outputParentDir !== null) {
logStatus(`Checking output directory in ${outputParentDir}`);
const outputPath = join(outputParentDir, `${pluginId}.jpl`);
const sourceFile = jplFiles[0];
logStatus(`Copying built file from ${sourceFile} to ${outputPath}`);
await copy(sourceFile, outputPath);
} else {
console.warn('No output directory specified. Not copying built .jpl files.');
}
} catch (error) {
console.error(error);
console.log('Build directory', buildDir);
await waitForCliInput();
throw error;
} finally {
chdir(originalDirectory);
await remove(buildDir);
logStatus('Removed build directory');
logStatus('Copying plugin', pluginId, 'JPL file to', outputPath);
await copy(join(__dirname, 'node_modules', repositoryData.package, 'publish', `${pluginId}.jpl`), outputPath);
logStatus('Copied.');
}
}
};
const logStatus = (...message: string[]) => {
const blue = '\x1b[96m';
const reset = '\x1b[0m';
console.log(blue, ...message, reset);
};
const buildPlugin = async (pluginId: string, repositoryData: RepositoryData, outputPath: string|null, options: Options) => {
const pluginSourcesDir = resolve(join(__dirname, 'plugin-sources'));
const originalDirectory = cwd();
const buildDir = await mkdtemp(join(tmpdir(), 'default-plugin-build'));
try {
logStatus('Building plugin', pluginId, 'at', buildDir);
const pluginDir = resolve(join(pluginSourcesDir, pluginId));
// Clone the repository if not done yet
if (!(await exists(pluginDir)) || (await readdir(pluginDir)).length === 0) {
logStatus(`Cloning from repository ${repositoryData.cloneUrl}`);
await execCommand(['git', 'clone', '--', repositoryData.cloneUrl, pluginDir]);
chdir(pluginDir);
}
chdir(pluginDir);
const expectedCommitHash = repositoryData.commit;
logStatus(`Switching to commit ${expectedCommitHash}`);
await execCommand(['git', 'switch', repositoryData.branch]);
try {
await execCommand(['git', 'checkout', expectedCommitHash]);
} catch (error) {
logStatus(`git checkout failed with error ${error}. Fetching...`);
await execCommand(['git', 'fetch']);
await execCommand(['git', 'checkout', expectedCommitHash]);
}
if (await getCurrentCommitHash() !== expectedCommitHash) {
throw new Error(`Unable to checkout commit ${expectedCommitHash}`);
}
logStatus('Copying repository files...');
await copy(pluginDir, buildDir, {
filter: fileName => {
return basename(fileName) !== '.git';
},
});
chdir(buildDir);
logStatus('Initializing repository.');
await execCommand('git init . -b main');
logStatus('Running before-patch hook.');
await options.beforePatch();
const patchFile = getPathToPatchFileFor(pluginId);
if (await exists(patchFile)) {
logStatus('Applying patch.');
await execCommand(['git', 'apply', patchFile]);
}
await options.beforeInstall(buildDir, pluginId);
logStatus('Installing dependencies.');
await execCommand('npm install');
const jplFiles = await glob('publish/*.jpl');
logStatus(`Found built .jpl files: ${JSON.stringify(jplFiles)}`);
if (jplFiles.length === 0) {
throw new Error(`No published files found in ${buildDir}/publish`);
}
if (outputPath !== null) {
const sourceFile = jplFiles[0];
logStatus(`Copying built file from ${sourceFile} to ${outputPath}`);
await copy(sourceFile, outputPath);
} else {
console.warn('No output directory specified. Not copying built .jpl files.');
}
} catch (error) {
console.error(error);
console.log('Build directory', buildDir);
await waitForCliInput();
throw error;
} finally {
chdir(originalDirectory);
await remove(buildDir);
logStatus('Removed build directory');
}
};
export default buildDefaultPlugins;

View File

@@ -1,7 +1,8 @@
import buildDefaultPlugins from '../buildDefaultPlugins';
const buildAll = (outputDirectory: string) => {
return buildDefaultPlugins(outputDirectory, {
return buildDefaultPlugins({
outputParentDir: outputDirectory,
beforeInstall: async () => { },
beforePatch: async () => { },
});

View File

@@ -8,7 +8,8 @@ import getPathToPatchFileFor from '../utils/getPathToPatchFileFor';
const editPatch = async (targetPluginId: string, outputParentDir: string|null) => {
let patchedPlugin = false;
await buildDefaultPlugins(outputParentDir, {
await buildDefaultPlugins({
outputParentDir: outputParentDir,
beforePatch: async () => {
// To make updating just the patch possible, a commit is created just before applying
// the patch.
@@ -34,7 +35,7 @@ const editPatch = async (targetPluginId: string, outputParentDir: string|null) =
});
if (!patchedPlugin) {
throw new Error(`No default plugin with ID ${targetPluginId} found!`);
throw new Error(`No patchable default plugin with ID ${targetPluginId} found! Make sure that the plugin has a "cloneUrl" and "branch" in pluginRepositories.json.`);
}
};

View File

@@ -14,6 +14,7 @@
},
"devDependencies": {
"@types/yargs": "17.0.33",
"joplin-plugin-freehand-drawing": "4.2.0",
"ts-node": "10.9.2",
"typescript": "5.8.3"
},

View File

@@ -2,11 +2,10 @@
"io.github.jackgruber.backup": {
"cloneUrl": "https://github.com/JackGruber/joplin-plugin-backup.git",
"branch": "master",
"commit": "abb58175e2d2bf34899f1b32cb74137e5c788bf9"
"commit": "2c3da7056e7ac39c86c2051a4fdb99d9534dd0a1"
},
"io.github.personalizedrefrigerator.js-draw": {
"cloneUrl": "https://github.com/personalizedrefrigerator/joplin-plugin-freehand-drawing.git",
"branch": "main",
"commit": "63b6d3f185b5b3664632e498df7c7ad7824038d0"
"package": "joplin-plugin-freehand-drawing"
}
}

View File

@@ -1,13 +1,28 @@
import { readFile } from 'fs-extra';
export enum BuiltInPluginType {
// Plugins that need to be built when building Joplin (e.g. if the plugin
// needs to be patched)
Built,
// Plugins that can be fetched directly from NPM. Must also be marked as a
// dev dependency.
FromNpm,
}
export interface RepositoryData {
type: BuiltInPluginType.Built;
cloneUrl: string;
branch: string;
commit: string;
}
export interface NpmReference {
type: BuiltInPluginType.FromNpm;
package: string;
}
export interface AllRepositoryData {
[pluginId: string]: RepositoryData;
[pluginId: string]: RepositoryData|NpmReference;
}
const readRepositoryJson = async (repositoryDataFilepath: string): Promise<AllRepositoryData> => {
@@ -26,9 +41,17 @@ const readRepositoryJson = async (repositoryDataFilepath: string): Promise<AllRe
}
};
assertPropertyIsString('cloneUrl');
assertPropertyIsString('branch');
assertPropertyIsString('commit');
let type;
if ('branch' in parsedJson[pluginId]) {
assertPropertyIsString('cloneUrl');
assertPropertyIsString('branch');
assertPropertyIsString('commit');
type = BuiltInPluginType.Built;
} else {
assertPropertyIsString('package');
type = BuiltInPluginType.FromNpm;
}
parsedJson[pluginId] = { ...parsedJson[pluginId], type };
}
return parsedJson;

View File

@@ -50,7 +50,7 @@ describe('createEditor', () => {
const headerLine = document.body.querySelector('.cm-headerLine')!;
expect(headerLine.textContent).toBe(headerLineText);
expect(getComputedStyle(headerLine).fontSize).toBe('1.6em');
expect(getComputedStyle(headerLine).fontSize).toBe('1.5em');
// CodeMirror nests the tag that styles the header within .cm-headerLine:
// <div class='cm-headerLine'><span class='someclass'>Testing...</span></div>

View File

@@ -3,20 +3,31 @@ import { SyntaxNodeRef } from '@lezer/common';
import makeReplaceExtension from './utils/makeInlineReplaceExtension';
import toggleCheckboxAt from '../../utils/markdown/toggleCheckboxAt';
const checkboxClassName = 'cm-ext-checkbox-toggle';
const checkboxContainerClassName = 'cm-ext-checkbox-toggle';
const checkboxClassName = 'cm-ext-checkbox';
class CheckboxWidget extends WidgetType {
public constructor(private checked: boolean, private depth: number, private label: string) {
public constructor(
private checked: boolean,
private depth: number,
private label: string,
private markup: string,
) {
super();
}
public eq(other: CheckboxWidget) {
return other.checked === this.checked && other.depth === this.depth && other.label === this.label;
return other.checked === this.checked
&& other.depth === this.depth
&& other.label === this.label
&& other.markup === this.markup;
}
private applyContainerClasses(container: HTMLElement) {
container.classList.add(checkboxClassName);
container.classList.add(checkboxContainerClassName);
// For sizing: Should have the same font/styles as non-rendered checkboxes:
container.classList.add('cm-taskMarker');
for (const className of [...container.classList]) {
if (className.startsWith('-depth-')) {
@@ -30,12 +41,22 @@ class CheckboxWidget extends WidgetType {
public toDOM(view: EditorView) {
const container = document.createElement('span');
const sizingNode = document.createElement('span');
sizingNode.classList.add('sizing');
sizingNode.textContent = this.markup;
container.appendChild(sizingNode);
const checkboxWrapper = document.createElement('span');
checkboxWrapper.classList.add('content');
container.appendChild(checkboxWrapper);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = this.checked;
checkbox.ariaLabel = this.label;
checkbox.title = this.label;
container.appendChild(checkbox);
checkbox.classList.add(checkboxClassName);
checkboxWrapper.appendChild(checkbox);
checkbox.oninput = () => {
toggleCheckboxAt(view.posAtDOM(container))(view);
@@ -66,16 +87,32 @@ const completedListItemDecoration = Decoration.line({ class: completedTaskClassN
const replaceCheckboxes = [
EditorView.theme({
[`& .${checkboxContainerClassName}`]: {
position: 'relative',
'& > .sizing': {
visibility: 'hidden',
},
'& > .content': {
position: 'absolute',
left: '0',
right: '0',
top: '0',
bottom: '0',
textAlign: 'center',
},
},
[`& .${checkboxClassName}`]: {
'& > input': {
width: '1.1em',
height: '1.1em',
margin: '4px',
verticalAlign: 'middle',
},
'&:not(.-depth-1) > input': {
marginInlineStart: 0,
},
verticalAlign: 'middle',
// Ensure that the checkbox grows as the font size increases:
width: '100%',
minHeight: '70%',
// Shift the checkbox slightly so that it's aligned with the list item bullet point
margin: '0',
marginBottom: '3px',
},
[`& .${completedTaskClassName}`]: {
opacity: 0.69,
@@ -84,7 +121,7 @@ const replaceCheckboxes = [
EditorView.domEventHandlers({
mousedown: (event) => {
const target = event.target as Element;
if (target.nodeName === 'INPUT' && target.parentElement?.classList?.contains(checkboxClassName)) {
if (target.nodeName === 'INPUT' && target.classList?.contains(checkboxClassName)) {
// Let the checkbox handle the event
return true;
}
@@ -101,8 +138,14 @@ const replaceCheckboxes = [
if (node.name === 'TaskMarker') {
const containerLine = state.doc.lineAt(node.from);
const labelText = state.doc.sliceString(node.to, containerLine.to);
const markerText = state.doc.sliceString(node.from, node.to);
return new CheckboxWidget(markerIsChecked(node), parentTags.get('ListItem') ?? 0, labelText);
return new CheckboxWidget(
markerIsChecked(node),
parentTags.get('ListItem') ?? 0,
labelText,
markerText,
);
} else if (node.name === 'Task') {
const marker = node.node.getChild('TaskMarker');
if (marker && markerIsChecked(marker)) {
@@ -119,7 +162,7 @@ const replaceCheckboxes = [
return null;
}
return [listMarker.from, node.to];
return [node.from, node.to];
} else if (node.name === 'Task') {
const taskLine = state.doc.lineAt(node.from);
return [taskLine.from];

View File

@@ -77,7 +77,9 @@ const makeBlockReplaceExtension = (extensionSpec: ReplacementExtension) => {
return extensionSpec.shouldFullReRender(transaction);
};
if (transaction.docChanged || selectionChanged || wasRerenderRequested()) {
const treeChanged = syntaxTree(transaction.state) !== syntaxTree(transaction.startState);
if (transaction.docChanged || selectionChanged || wasRerenderRequested() || treeChanged) {
decorations = updateDecorations(transaction.state, extensionSpec);
}

View File

@@ -86,6 +86,7 @@ const createTheme = (theme: EditorTheme): Extension[] => {
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
paddingBottom: '0.2em',
};
const codeMirrorTheme = EditorView.theme({
@@ -210,7 +211,12 @@ const createTheme = (theme: EditorTheme): Extension[] => {
// small.
'& .cm-h1': {
...baseHeadingStyle,
fontSize: '1.6em',
fontSize: '1.5em',
},
// Only underline level 1 headings not in block quotes. The underline overlaps with the blockquote border.
'& .cm-h1:not(.cm-blockQuote)': {
borderBottom: `1px solid ${theme.dividerColor}`,
marginBottom: '0.1em',
},
'& .cm-h2': {
...baseHeadingStyle,

View File

@@ -135,9 +135,12 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
if (view) {
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
const content = selectedText || '...';
return showCreateEditablePrompt(
block ? `$$\n\t${content}\n$$` : `$${content}$`, !block,
)(state, dispatch, view);
const blockStart = block ? '$$\n\t' : '$';
return showCreateEditablePrompt({
source: block ? `${blockStart}${content}\n$$` : `${blockStart}${content}$`,
inline: !block,
cursor: blockStart.length,
})(state, dispatch, view);
}
return true;
}
@@ -180,10 +183,13 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
return true;
},
[EditorCommandType.InsertCodeBlock]: (state, dispatch, view) => {
const sourceBlockStart = '```\n';
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
return showCreateEditablePrompt(
`\`\`\`\n${selectedText}\n\`\`\``, false,
)(state, dispatch, view);
return showCreateEditablePrompt({
source: `${sourceBlockStart}${selectedText}\n\`\`\``,
inline: false,
cursor: sourceBlockStart.length,
})(state, dispatch, view);
},
[EditorCommandType.ToggleSearch]: (state, dispatch, view) => {
const command = setSearchVisible(!getSearchVisible(state));

View File

@@ -4,19 +4,20 @@ import { MarkType, ResolvedPos } from 'prosemirror-model';
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { closeHistory } from 'prosemirror-history';
import showCreateEditablePrompt from './joplinEditablePlugin/showCreateEditablePrompt';
interface InlineInputRule {
interface InputRuleData {
match: RegExp;
matchEndCharacter: string;
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number, commitCharacter: string)=> Transaction|null;
commitCharacter: string|null;
handler: (view: EditorView, match: RegExpMatchArray, start: number, end: number, commitCharacter: string)=> Transaction|null;
}
// A custom input rule extension for inline input replacements.
//
// Ref: https://github.com/ProseMirror/prosemirror-inputrules/blob/43ef04ce9c1512ef8f2289578309c40b431ed3c5/src/inputrules.ts#L82
// See https://discuss.prosemirror.net/t/trigger-inputrule-on-enter/1118 for why this approach is needed.
const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: RegExp) => {
const inlineInputRules = (rules: InputRuleData[], commitCharacterExpression: RegExp) => {
const getContentBeforeCursor = (cursorInformation: ResolvedPos) => {
const parent = cursorInformation.parent;
const offsetInParent = cursorInformation.parentOffset;
@@ -25,9 +26,11 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
};
const getApplicableRule = (state: EditorState, cursor: number, justTypedText: string) => {
if (!rules.some(rule => justTypedText.endsWith(rule.matchEndCharacter))) {
const candidateRules = rules.filter(rule => justTypedText.endsWith(rule.commitCharacter ?? ''));
if (!candidateRules.length) {
return false;
}
const cursorInformation = state.doc.resolve(cursor);
const inCode = cursorInformation.parent.type.spec.code;
if (inCode) {
@@ -35,7 +38,7 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
}
const beforeCursor = getContentBeforeCursor(cursorInformation) + justTypedText;
for (const rule of rules) {
for (const rule of candidateRules) {
const match = beforeCursor.match(rule.match);
if (!match) continue;
@@ -44,7 +47,7 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
return null;
};
type PluginState = { pendingRule: InlineInputRule };
type PluginState = { pendingRule: InputRuleData };
const run = (view: EditorView, cursor: number, commitData: string) => {
const commitCharacter = commitCharacterExpression.exec(commitData) ? commitData : '';
@@ -57,7 +60,7 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
const beforeCursor = getContentBeforeCursor(view.state.doc.resolve(cursor));
const match = beforeCursor.match(availableRule.match);
if (match) {
const transaction = availableRule.handler(view.state, match, cursor - match[0].length, cursor, commitCharacter);
const transaction = availableRule.handler(view, match, cursor - match[0].length, cursor, commitCharacter);
if (transaction) {
// closeHistory: Move the markup completion to a separate history event so that it
// can be undone separately.
@@ -109,17 +112,25 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
return plugin;
};
const makeMarkInputRule = (
regExpString: string, matchEndCharacter: string, replacement: (matches: RegExpMatchArray)=> string, mark: MarkType,
): InlineInputRule => {
type OnReplace = (matches: RegExpMatchArray, view: EditorView)=> string;
interface InputRuleOptions {
contentRegex: string|RegExp;
commitCharacter: string|null; // null => only "Enter"
onReplace: OnReplace;
marks: MarkType[];
}
const makeInputRule = ({
contentRegex, commitCharacter, onReplace, marks,
}: InputRuleOptions): InputRuleData => {
const commitCharacterExp = '[.?!,:;¡¿() \\n]';
const regex = new RegExp(`(^|${commitCharacterExp})${regExpString}$`);
const regex = typeof contentRegex === 'string' ? new RegExp(`(^|${commitCharacterExp})${contentRegex}$`) : contentRegex;
return {
match: regex,
matchEndCharacter,
handler: (state, match, start, end, endCommitCharacter) => {
commitCharacter,
handler: (view, match, start, end, endCommitCharacter) => {
const state = view.state;
let transaction = state.tr.delete(start, end);
const marks = [schema.mark(mark)];
const startCommitCharacter = match[1];
@@ -131,13 +142,12 @@ const makeMarkInputRule = (
];
matchesWithoutCommitCharacters.groups = match.groups;
const replacementText = replacement(matchesWithoutCommitCharacters);
const replacement = onReplace(matchesWithoutCommitCharacters, view);
transaction = transaction.insert(
transaction.mapping.map(start, -1),
[
!!startCommitCharacter && schema.text(startCommitCharacter),
!!replacementText && schema.text(replacementText, marks),
!!replacement && schema.text(replacement, marks.map(type => schema.mark(type))),
!!endCommitCharacter && schema.text(endCommitCharacter),
].filter(node => !!node),
);
@@ -149,33 +159,51 @@ const makeMarkInputRule = (
const baseInputRules = buildInputRules(schema);
const inlineContentExp = '\\S[^\\n]*\\S|\\S';
const noMatchRegex = /$^/;
const inputRulesExtension = [
baseInputRules,
inlineInputRules([
makeMarkInputRule(
`\\*\\*(${inlineContentExp})\\*\\*`,
'*',
(match) => match[1],
schema.marks.strong,
),
makeMarkInputRule(
`\\*(${inlineContentExp})\\*`,
'*',
(match) => match[1],
schema.marks.emphasis,
),
makeMarkInputRule(
`_(${inlineContentExp})_`,
'_',
(match) => match[1],
schema.marks.emphasis,
),
makeMarkInputRule(
`[\`](${inlineContentExp})[\`]`,
'`',
(match) => match[1],
schema.marks.code,
),
makeInputRule({
contentRegex: /(^|[\n])(```+)(\w*)$/,
commitCharacter: '',
onReplace: (match, view) => {
const blockStart = `${match[1]}${match[2]}\n`;
const block = `${blockStart}\n${match[1]}`;
showCreateEditablePrompt({
source: block,
inline: false,
cursor: blockStart.length,
})(view.state, view.dispatch, view);
return '';
},
marks: [],
}),
], noMatchRegex),
inlineInputRules([
makeInputRule({
contentRegex: `\\*\\*(${inlineContentExp})\\*\\*`,
commitCharacter: '*',
onReplace: (match) => match[1],
marks: [schema.marks.strong],
}),
makeInputRule({
contentRegex: `\\*(${inlineContentExp})\\*`,
commitCharacter: '*',
onReplace: (match) => match[1],
marks: [schema.marks.emphasis],
}),
makeInputRule({
contentRegex: `_(${inlineContentExp})_`,
commitCharacter: '_',
onReplace: (match) => match[1],
marks: [schema.marks.emphasis],
}),
makeInputRule({
contentRegex: `\`(${inlineContentExp})\``,
commitCharacter: '`',
onReplace: (match) => match[1],
marks: [schema.marks.code],
}),
], /[ .,?)!;]/),
];
export default inputRulesExtension;

View File

@@ -23,13 +23,15 @@ const createEditorDialogForNode = (nodePosition: number, view: EditorView, onHid
view.state.doc.nodeAt(nodePosition)
);
const openCharacters = getNode().attrs.openCharacters ?? '';
const { dismiss } = createEditorDialog({
editorApi: getEditorApi(view.state),
source: [
getNode().attrs.openCharacters,
openCharacters,
getNode().attrs.source,
getNode().attrs.closeCharacters,
getNode().attrs.closeCharacters ?? '',
].join(''),
cursor: openCharacters.length,
onSave: async (source) => {
view.dispatch(
view.state.tr.setNodeAttribute(

View File

@@ -38,7 +38,11 @@ describe('showCreateEditorPrompt', () => {
test('should allow creating a new code block', async () => {
const editor = createEditor('');
showCreateEditablePrompt('```\ntest\n```', false)(editor.state, editor.dispatch, editor);
showCreateEditablePrompt({
source: '```\ntest\n```',
inline: false,
cursor: 8,
})(editor.state, editor.dispatch, editor);
const dialog = findEditorDialog();
dialog.submitButton.click();
@@ -54,4 +58,16 @@ describe('showCreateEditorPrompt', () => {
}],
});
});
test('should position the cursor at the provided location', () => {
const editor = createEditor('');
showCreateEditablePrompt({
source: '```\n\n```',
inline: false,
cursor: 4,
})(editor.state, editor.dispatch, editor);
const dialog = findEditorDialog();
expect(dialog.editor.selectionStart).toBe(4);
});
});

View File

@@ -5,13 +5,20 @@ import postProcessRenderedHtml from './utils/postProcessRenderedHtml';
import schema from '../../schema';
import { JoplinEditableAttributes } from './joplinEditablePlugin';
const showCreateEditablePrompt = (source: string, inline: boolean): Command => (_state, dispatch, view) => {
interface EditablePromptOptions {
source: string;
inline: boolean;
cursor: number;
}
const showCreateEditablePrompt = ({ source, inline, cursor }: EditablePromptOptions): Command => (_state, dispatch, view) => {
if (!dispatch) return true;
if (!view) throw new Error('Missing required argument: view');
createEditorDialog({
editorApi: getEditorApi(view.state),
source,
cursor,
onSave: async (newSource) => {
source = newSource;
},

View File

@@ -1,15 +1,17 @@
import { EditorApi } from '../../joplinEditorApiPlugin';
import { EditorLanguageType } from '../../../../types';
import showModal from '../../../utils/dom/showModal';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Options {
source: string;
cursor: number;
editorApi: EditorApi;
onSave: (newContent: string)=> void;
onDismiss: ()=> void;
}
const createEditorDialog = ({ editorApi, source, onSave, onDismiss }: Options) => {
const createEditorDialog = ({ editorApi, source, cursor, onSave, onDismiss }: Options) => {
const content = document.createElement('div');
content.classList.add('editor-dialog-content');
document.body.appendChild(content);
@@ -22,9 +24,10 @@ const createEditorDialog = ({ editorApi, source, onSave, onDismiss }: Options) =
},
);
editor.updateBody(source);
editor.select(cursor, cursor);
const _ = editorApi.localize;
return showModal({
const modal = showModal({
content,
doneLabel: _('Done'),
onDismiss: () => {
@@ -32,6 +35,10 @@ const createEditorDialog = ({ editorApi, source, onSave, onDismiss }: Options) =
editor.remove();
},
});
focus('createEditorDialog', editor);
return modal;
};
export default createEditorDialog;

View File

@@ -48,6 +48,9 @@ const joplinEditorApiPlugin = new Plugin<EditorApi>({
updateBody: (newValue) => {
editor.textArea.value = newValue;
},
select: (anchor, head) => {
editor.textArea.setSelectionRange(Math.min(anchor, head), Math.max(anchor, head));
},
};
},
}),

View File

@@ -18,6 +18,7 @@ export interface CodeEditorControl {
focus: ()=> void;
remove: ()=> void;
updateBody: (newValue: string)=> void;
select: (from: number, to: number)=> void;
}
export type OnCodeEditorChange = (newValue: string)=> void;

View File

@@ -0,0 +1,24 @@
import { convertValuesToFunctions, sortByValue } from './ObjectUtils';
describe('ObjectUtils', () => {
test('should convert object values to functions', () => {
const v = convertValuesToFunctions({ a: 6, b: ()=>7, c: 'test' });
expect(v.a()).toBe(6);
expect(v.b()).toBe(7);
expect(v.c()).toBe('test');
});
test('should sort an object\'s entries by value', () => {
expect(sortByValue({
a: 1,
b: 'test1',
c: 'test3',
d: 'test2',
})).toEqual({
b: 'test1',
d: 'test2',
c: 'test3',
a: 1,
});
});
});

View File

@@ -1,5 +1,9 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export function sortByValue(object: any) {
type AnyObject = Record<string|symbol, unknown>;
type SortableValue = string|number;
type SortableObject = Record<string, SortableValue>;
export function sortByValue<T extends SortableObject>(object: T): T {
const temp = [];
for (const k in object) {
if (!object.hasOwnProperty(k)) continue;
@@ -10,8 +14,8 @@ export function sortByValue(object: any) {
}
temp.sort((a, b) => {
let v1 = a.value;
let v2 = b.value;
let v1: SortableValue = a.value;
let v2: SortableValue = b.value;
if (typeof v1 === 'string') v1 = v1.toLowerCase();
if (typeof v2 === 'string') v2 = v2.toLowerCase();
if (v1 === v2) return 0;
@@ -28,8 +32,7 @@ export function sortByValue(object: any) {
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export function fieldsEqual(o1: any, o2: any) {
export function fieldsEqual(o1: AnyObject, o2: AnyObject) {
if ((!o1 || !o2) && o1 !== o2) return false;
for (const k in o1) {
@@ -45,8 +48,11 @@ export function fieldsEqual(o1: any, o2: any) {
return true;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export function convertValuesToFunctions(o: any) {
type ValuesToFunctions<T extends AnyObject> = {
[k in keyof T]: T[k] extends ()=> unknown ? T[k] : ()=> T[k]
};
export function convertValuesToFunctions<T extends AnyObject>(o: T): ValuesToFunctions<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const output: any = {};
for (const n in o) {
@@ -58,8 +64,7 @@ export function convertValuesToFunctions(o: any) {
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export function isEmpty(o: any) {
export function isEmpty(o: AnyObject|null) {
if (!o) return true;
return Object.keys(o).length === 0 && o.constructor === Object;
}

View File

@@ -1,6 +1,5 @@
import Setting, { AppType, SettingMetadataSection, SettingSectionSource } from '../../../models/Setting';
import SyncTargetRegistry from '../../../SyncTargetRegistry';
const ObjectUtils = require('../../../ObjectUtils');
const { _ } = require('../../../locale');
import { createSelector } from 'reselect';
import Logger from '@joplin/utils/Logger';
@@ -8,6 +7,7 @@ import Logger from '@joplin/utils/Logger';
import { type ReactNode } from 'react';
import { type Registry } from '../../../registry';
import settingValidations from '../../../models/settings/settingValidations';
import { convertValuesToFunctions } from '../../../ObjectUtils';
const logger = Logger.create('config-shared');
@@ -75,11 +75,11 @@ export const checkSyncConfig = async (comp: ConfigScreenComponent, settings: any
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
const options = {
...Setting.subValues(`sync.${syncTargetId}`, settings),
...Setting.subValues('net', settings) };
...Setting.subValues(`sync.${syncTargetId}`, settings, { includeConstants: true }),
...Setting.subValues('net', settings, { includeConstants: true }) };
comp.setState({ checkSyncConfigResult: 'checking' });
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
const result = await SyncTargetClass.checkConfig(convertValuesToFunctions(options));
comp.setState({ checkSyncConfigResult: result });
if (result.ok) {

View File

@@ -171,6 +171,11 @@ interface UserSettingMigration {
isPluginSetting: boolean;
}
interface SubValuesOptions {
includeBaseKeyInName?: boolean;
includeConstants?: boolean;
}
const userSettingMigration: UserSettingMigration[] = [
{
oldName: 'spellChecker.language',
@@ -1100,18 +1105,29 @@ class Setting extends BaseModel {
// and baseKey is 'sync.5', the function will return
// { path: 'http://example', username: 'testing' }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public static subValues(baseKey: string, settings: Partial<SettingsRecord>, options: any = null) {
public static subValues(baseKey: string, settings: Partial<SettingsRecord>, options: SubValuesOptions|null = null) {
const includeBaseKeyInName = !!options && !!options.includeBaseKeyInName;
const subKey = (key: string) => {
return includeBaseKeyInName ? key : key.substring(baseKey.length + 1);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const output: any = {};
for (const key in settings) {
if (!settings.hasOwnProperty(key)) continue;
if (key.indexOf(baseKey) === 0) {
const subKey = includeBaseKeyInName ? key : key.substr(baseKey.length + 1);
output[subKey] = settings[key];
for (const [key, value] of Object.entries(settings)) {
if (key.startsWith(baseKey)) {
output[subKey(key)] = value;
}
}
if (options?.includeConstants) {
for (const [key, value] of Object.entries(this.constants_)) {
if (key.startsWith(baseKey)) {
output[subKey(key)] = value;
}
}
}
return output;
}

View File

@@ -1118,7 +1118,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
label: () => `${_('Enable file:// URLs for images and videos')}${wysiwygYes}`,
},
'markdown.plugin.abc.options': { storage: SettingStorage.File, isGlobal: true, value: '', type: SettingItemType.String, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('ABC musical notation: Options')}${wysiwygNo}`, description: () => _('Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %1', 'https://paulrosen.github.io/abcjs/visual/render-abc-options.html') },
'markdown.plugin.abc.options': { storage: SettingStorage.File, isGlobal: true, value: '', type: SettingItemType.String, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('ABC musical notation: Options')}${wysiwygNo}`, description: () => _('Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s', 'https://paulrosen.github.io/abcjs/visual/render-abc-options.html') },
// Tray icon (called AppIndicator) doesn't work in Ubuntu
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html

View File

@@ -231,7 +231,7 @@ describe('InteropService_Importer_OneNote', () => {
const noteToTest = notes.find(n => n.title === 'Tips from a Pro Using Trees for Dramatic Landscape Photography');
expectWithInstructions(noteToTest).toBeTruthy();
expectWithInstructions(noteToTest.body).toContain('<a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/%E9%A3%8E%E6%99%AF.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a>');
expectWithInstructions(noteToTest.body).toContain('<a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/%E9%A3%8E%E6%99%AF.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&amp;section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&amp;page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&amp;end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a>');
});
it('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {

View File

@@ -69,6 +69,8 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
// other files.
fileNamePattern: '*.one',
});
await this.fixIncorrectLatin1Decoding_(extractPath);
} else {
throw new Error(`Unknown file extension: ${fileExtension}`);
}
@@ -177,6 +179,7 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
const pipeline = [
(dom: Document, currentFolder: string) => this.extractSvgsToFiles_(dom, currentFolder),
(dom: Document, currentFolder: string) => this.convertExternalLinksToInternalLinks_(dom, currentFolder),
(dom: Document, _currentFolder: string) => Promise.resolve(this.simplifyHtml_(dom)),
];
for (const file of htmlFiles) {
@@ -234,6 +237,28 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
return changed;
}
private simplifyHtml_(dom: Document) {
const selectors = [
// <script> blocks that aren't marked with a specific type (e.g. application/tex).
'script:not([type])',
// ID mappings (unused at this stage of the import process)
'meta[name="X-Original-Page-Id"]',
// Empty iframes
'iframe[src=""]',
];
let changed = false;
for (const selector of selectors) {
for (const element of dom.querySelectorAll(selector)) {
element.remove();
changed = true;
}
}
return changed;
}
private async extractSvgsToFiles_(dom: Document, svgBaseFolder: string) {
const { svgs, changed } = this.extractSvgs(dom);
@@ -295,4 +320,47 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
changed: true,
};
}
// Works around a decoding issue in which file names are extracted as latin1 strings,
// rather than UTF-8 strings. For example, OneNote seems to encode filenames as UTF-8 in .onepkg files.
// However, EXPAND.EXE reads the filenames as latin1. As a result, "é.one" becomes
// "é.one" when extracted from the archive.
// This workaround re-encodes filenames as UTF-8.
private async fixIncorrectLatin1Decoding_(parentDir: string) {
// Only seems to be necessary on Windows.
if (!shim.isWindows()) return;
const fixEncoding = async (basePath: string, fileName: string) => {
const originalPath = join(basePath, fileName);
let newPath;
let fixedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
if (fixedFileName !== fileName) {
// In general, the path shouldn't start with "."s or contain path separators.
// However, if it does, these characters might cause import errors, so remove them:
fixedFileName = fixedFileName.replace(/^\.+/, '');
fixedFileName = fixedFileName.replace(/[/\\]/g, ' ');
// Avoid path traversal: Ensure that the file path is contained within the base directory
const newFullPathSafe = shim.fsDriver().resolveRelativePathWithinDir(basePath, fixedFileName);
await shim.fsDriver().move(originalPath, newFullPathSafe);
newPath = newFullPathSafe;
} else {
newPath = originalPath;
}
if (await shim.fsDriver().isDirectory(originalPath)) {
const children = await shim.fsDriver().readDirStats(newPath, { recursive: false });
for (const child of children) {
await fixEncoding(originalPath, child.path);
}
}
};
const stats = await shim.fsDriver().readDirStats(parentDir, { recursive: false });
for (const stat of stats) {
await fixEncoding(parentDir, stat.path);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -77,13 +77,19 @@ impl DataElementPackage {
cell: ExGuid,
storage_index: &StorageIndex,
) -> Result<Vec<&ObjectGroup>> {
let revision_id = self
.find_cell_revision_id(cell)
.ok_or_else(|| ErrorKind::MalformedFssHttpBData("cell revision id not found".into()))?;
let revision_mapping_id = storage_index
.find_revision_mapping_id(revision_id)
let revision_mapping_id = self
.resolve_cell_revision_manifest_id(storage_index, cell)
.or_else(|| {
storage_index
.cell_mappings
.values()
.find(|mapping| mapping.id == cell)
.and_then(|mapping| {
storage_index.find_revision_mapping_by_serial(&mapping.serial)
})
})
.ok_or_else(|| {
ErrorKind::MalformedFssHttpBData("revision mapping id not found".into())
ErrorKind::MalformedFssHttpBData("revision manifest id not found".into())
})?;
let revision_manifest = self
.find_revision_manifest(revision_mapping_id)
@@ -112,16 +118,46 @@ impl DataElementPackage {
self.storage_indexes.values().next()
}
/// Look up a storage index by its ID.
pub(crate) fn find_storage_index_by_id(&self, id: ExGuid) -> Option<&StorageIndex> {
self.storage_indexes.get(&id)
}
/// Find the first storage manifest.
pub(crate) fn find_storage_manifest(&self) -> Option<&StorageManifest> {
self.storage_manifests.values().next()
}
/// Resolve a revision manifest ID, falling back when revision mappings are missing.
pub(crate) fn resolve_revision_manifest_id(
&self,
storage_index: &StorageIndex,
id: ExGuid,
) -> Option<ExGuid> {
storage_index
.find_revision_mapping_id(id)
.or_else(|| self.revision_manifests.get(&id).map(|_| id))
}
/// Look up a cell revision ID by the cell's manifest ID.
pub(crate) fn find_cell_revision_id(&self, id: ExGuid) -> Option<ExGuid> {
self.cell_manifests.get(&id).copied()
}
/// Resolve a cell's revision manifest ID, with a fallback for newer files.
pub(crate) fn resolve_cell_revision_manifest_id(
&self,
storage_index: &StorageIndex,
cell_manifest_id: ExGuid,
) -> Option<ExGuid> {
let resolved = self
.find_cell_revision_id(cell_manifest_id)
.and_then(|revision_id| self.resolve_revision_manifest_id(storage_index, revision_id))
.or_else(|| self.resolve_revision_manifest_id(storage_index, cell_manifest_id));
resolved
}
/// Look up a revision manifest by its ID.
pub(crate) fn find_revision_manifest(&self, id: ExGuid) -> Option<&RevisionManifest> {
self.revision_manifests.get(&id)
@@ -189,7 +225,7 @@ impl DataElement {
return Err(ErrorKind::MalformedFssHttpBData(
format!("invalid element type: 0x{:X}", x).into(),
)
.into())
.into());
}
}

View File

@@ -32,6 +32,15 @@ impl StorageIndex {
.get(&id)
.map(|mapping| mapping.revision_mapping)
}
pub(crate) fn find_revision_mapping_by_serial(&self, serial: &SerialNumber) -> Option<ExGuid> {
self.revision_mappings
.values()
.find(|mapping| {
mapping.serial.guid == serial.guid && mapping.serial.serial == serial.serial
})
.map(|mapping| mapping.revision_mapping)
}
}
/// A storage indexes manifest mapping.

View File

@@ -71,7 +71,8 @@ pub(crate) fn parse_store(package: &OneStorePackaging) -> Result<FssHttpbOneStor
// [ONESTORE] 2.7.1: Parse storage manifest
let storage_index = package
.data_element_package
.find_storage_index()
.find_storage_index_by_id(package.storage_index)
.or_else(|| package.data_element_package.find_storage_index())
.ok_or_else(|| ErrorKind::MalformedOneStoreData("storage index is missing".into()))?;
let storage_manifest = package
.data_element_package

View File

@@ -7,7 +7,7 @@ use crate::fsshttpb_onestore::revision_role::RevisionRole;
use crate::onestore;
use crate::shared::cell_id::CellId;
use crate::shared::exguid::ExGuid;
use parser_utils::errors::Result;
use parser_utils::errors::{ErrorKind, Result};
use std::collections::HashMap;
pub(crate) type GroupData<'a> = HashMap<(ExGuid, u64), &'a ObjectGroupData>;
@@ -58,16 +58,31 @@ impl<'b> ObjectSpace {
let context_id = cell_id.0;
let object_space_id = cell_id.1;
let cell_manifest_id = ObjectSpace::find_cell_manifest_id(mapping.id, packaging)
.ok_or_else(|| parser_error!(MalformedOneStoreData, "cell manifest id not found"))?;
let revision_manifest_id = storage_index
.find_revision_mapping_id(cell_manifest_id)
.ok_or_else(|| {
parser_error!(
MalformedOneStoreData,
"No revision manifest id found. (Unable to find revision?)."
)
})?;
let cell_revision_id = packaging
.data_element_package
.find_cell_revision_id(mapping.id);
let revision_manifest_id = packaging
.data_element_package
.resolve_cell_revision_manifest_id(storage_index, mapping.id)
.or_else(|| storage_index.find_revision_mapping_by_serial(&mapping.serial));
if revision_manifest_id.is_none() && cell_revision_id.map(|id| id.is_nil()).unwrap_or(false)
{
return Ok((
cell_id,
ObjectSpace {
id: object_space_id,
context: context_id,
roots: HashMap::new(),
objects: HashMap::new(),
},
));
}
let revision_manifest_id = revision_manifest_id.ok_or_else(|| {
ErrorKind::MalformedOneStoreData("no revision manifest id found".into())
})?;
let mut objects = HashMap::new();
let mut roots = HashMap::new();
@@ -98,15 +113,4 @@ impl<'b> ObjectSpace {
Ok((cell_id, space))
}
fn find_cell_manifest_id(
cell_manifest_id: ExGuid,
packaging: &OneStorePackaging,
) -> Option<ExGuid> {
packaging
.data_element_package
.cell_manifests
.get(&cell_manifest_id)
.copied()
}
}

View File

@@ -42,10 +42,11 @@ impl Revision {
.base_rev_id
.as_option()
.map(|mapping_id| {
storage_index
.find_revision_mapping_id(mapping_id)
packaging
.data_element_package
.resolve_revision_manifest_id(storage_index, mapping_id)
.ok_or_else(|| {
ErrorKind::MalformedOneStoreData("revision mapping not found".into())
ErrorKind::MalformedOneStoreData("revision manifest id not found".into())
})
})
.transpose()?;

View File

@@ -25,10 +25,14 @@ impl<'a> Renderer<'a> {
let file_type = Self::guess_type(file);
match file_type {
// TODO: we still don't have support for the audio tag on html notes https://github.com/laurent22/joplin/issues/11939
// TODO: As of 01-06-2026, Joplin has limited or no support for <video> and <audio> elements in HTML notes.
// For example, <video> elements can only reference web URLs and <audio> elements aren't
// supported at all.
//
// See also: https://github.com/laurent22/joplin/issues/11939.
// FileType::Audio => content = format!("<audio class=\"media-player media-audio\"controls><source src=\"{}\" type=\"audio/x-wav\"></source></audio>", filename),
FileType::Video => content = format!("<video controls src=\"{}\" {}></video>", filename, styles.to_html_attr()),
FileType::Unknown | FileType::Audio => {
// FileType::Video => content = format!("<video controls src=\"{}\" {}></video>", filename, styles.to_html_attr()),
FileType::Unknown | FileType::Audio | FileType::Video => {
styles.set("font-size", "11pt".into());
styles.set("line-height", "17px".into());
let style_attr = styles.to_html_attr();

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<title>{{ name }}</title>

View File

@@ -2,14 +2,22 @@ const { execCommand } = require('@joplin/utils');
const yargs = require('yargs');
async function main() {
if (!process.env.IS_CONTINUOUS_INTEGRATION) {
const argv = yargs.argv;
if (!argv.profile) throw new Error('OneNote build: profile value is missing');
if (!['release', 'dev'].includes(argv.profile)) throw new Error('OneNote build: profile value is invalid');
const isDevBuild = argv.profile === 'dev';
if (!isDevBuild && !process.env.IS_CONTINUOUS_INTEGRATION) {
// eslint-disable-next-line no-console
console.info(
console.info([
'----------------------------------------------------------------\n' +
'Not building onenote-converter because it is not a continuous integration environment.\n' +
'Use IS_CONTINUOUS_INTEGRATION=1 env var if build is necessary.\n' +
'Not building onenote-converter because it is not a continuous integration environment.',
'',
'Either:',
' - Re-run with the IS_CONTINUOUS_INTEGRATION environment variable set to 1.',
' - Run "yarn buildDev" to create a development build.',
'----------------------------------------------------------------',
);
].join('\n'));
return;
}
@@ -20,15 +28,12 @@ async function main() {
return;
}
const argv = yargs.argv;
if (!argv.profile) throw new Error('OneNote build: profile value is missing');
if (!['release', 'dev'].includes(argv.profile)) throw new Error('OneNote build: profile value is invalid');
const buildCommand = `wasm-pack build --target nodejs --${argv.profile} ./renderer`;
await execCommand(buildCommand);
if (argv.profile !== 'release') return;
if (isDevBuild) return;
// If release build, remove intermediary folder to decrease size of release
const removeIntermediaryFolder = 'cargo clean';

View File

@@ -52,25 +52,33 @@ const plugin = (markdownIt: MarkdownIt, ruleOptions: any) => {
const token = tokens[idx];
if (token.info !== 'abc') return defaultRender(tokens, idx, options, env, self);
const escapeHtml = markdownIt.utils.escapeHtml;
ruleOptions.context.pluginWasUsed.abc = true;
try {
const parsed = parseAbcContent(token.content);
const globalOptions = ruleOptions.globalSettings ? parseGlobalOptions(ruleOptions.globalSettings['markdown.plugin.abc.options']) : {};
const contentHtml = markdownIt.utils.escapeHtml(parsed.markup.trim());
const optionsHtml = markdownIt.utils.escapeHtml(JSON.stringify({
const content = parsed.markup.trim();
const contentHtml = escapeHtml(content);
const optionsHtml = escapeHtml(JSON.stringify({
...globalOptions,
...parsed.options,
}));
const sourceContentLines: string[] = [];
if (parsed.options && Object.keys(parsed.options).length) sourceContentLines.push(JSON5.stringify(parsed.options));
sourceContentLines.push(content);
const sourceContentHtml = escapeHtml(sourceContentLines.join('\n---\n'));
return `
<div class="joplin-editable joplin-abc-notation">
<pre class="joplin-source" data-abc-options="${optionsHtml}" data-joplin-language="abc" data-joplin-source-open="\`\`\`abc&#10;" data-joplin-source-close="&#10;\`\`\`&#10;">${contentHtml}</pre>
<pre class="joplin-source" data-abc-options="${optionsHtml}" data-joplin-language="abc" data-joplin-source-open="\`\`\`abc&#10;" data-joplin-source-close="&#10;\`\`\`&#10;">${sourceContentHtml}</pre>
<pre class="joplin-rendered joplin-abc-notation-rendered">${contentHtml}</pre>
</div>
`;
} catch (error) {
return `<div class="inline-code">${markdownIt.utils.escapeHtml(error.message)}</div}>`;
return `<div class="inline-code">${escapeHtml(error.message)}</div}>`;
}
};
};

File diff suppressed because one or more lines are too long

View File

@@ -41,7 +41,7 @@
"html-entities": "1.4.0",
"json-stringify-safe": "5.0.1",
"json5": "2.2.3",
"katex": "0.16.22",
"katex": "0.16.23",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.3.0",

View File

@@ -226,3 +226,4 @@ xhdpi
xxhdpi
xxxhdpi
scrollend
fgag

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,17 @@
# Joplin Android Changelog
## [android-v3.5.7](https://github.com/laurent22/joplin/releases/tag/android-v3.5.7) (Pre-release) - 2026-01-08T19:30:28Z
- New: Rich Text Editor: Add shortcuts for inserting code blocks (#14055 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Accessibility: In-editor rendering: Fix rendered checkboxes are very small on mobile (#14056 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Markdown editor: Make header styles more closely match the note viewer (#14053) (#13753 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Markdown editor: Prevent layout shift when hiding/showing rendered checkboxes (#14044) (#13159 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Updated packages @rollup/plugin-node-resolve (v16.0.2), katex (v0.16.23)
- Fixed: Fix "Check synchronization configuration" button (#14031) (#14030 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix ABC Sheet Music setting includes "Translation error" in description (#14058) (#14049 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Images sometimes don't render until you click somewhere in the note (#14019) (#13963 by [@bwat47](https://github.com/bwat47))
- Fixed: Insert time command not respecting locale settings (#13994) (#13229 by [@HIJOdelIDANII](https://github.com/HIJOdelIDANII))
## [android-v3.5.6](https://github.com/laurent22/joplin/releases/tag/android-v3.5.6) (Pre-release) - 2025-12-27T20:34:44Z
- Revert "All: Apache Tomcat WebDAV compatibility for sync (#13614)"

View File

@@ -9,6 +9,7 @@ WebDAV-compatible services that are known to work with Joplin:
- [Fastmail](https://www.fastmail.com/)
- [HiDrive](https://www.strato.fr/stockage-en-ligne/) from Strato. [Setup help](https://github.com/laurent22/joplin/issues/309)
- [InfiniCLOUD](https://infini-cloud.net/)
- [Mailbox.org WebDAV](https://kb.mailbox.org/en/private/drive/) [Setup help](https://userforum-en.mailbox.org/topic/2766-unable-to-sync-joplin-notes-with-mailbox-org-via-webdav#comment-10946)
- [Nginx WebDAV Module](https://nginx.org/en/docs/http/ngx_http_dav_module.html)
- [Nextcloud](https://nextcloud.com/)
- [OwnCloud](https://owncloud.org/)

View File

@@ -76,18 +76,12 @@ Your Contributions.
waived such rights for your Contributions to the Company, or that your
employer has executed a separate Corporate CLA with the Company.
5. You represent that each of Your Contributions is Your original creation or
that You otherwise have the lawful right to submit it, and that each
Contribution is a work of human authorship. You represent that, although
automated or artificial intelligence tools may have been used to assist in
the creation of a Contribution, You have exercised creative judgment in
reviewing, selecting, modifying, or arranging the Contribution, and that no
Contribution is submitted that was generated entirely by automated means
without meaningful human involvement. You represent that Your Contribution
submissions include complete details of any third-party license or other
restriction (including, but not limited to, related patents and trademarks)
of which you are personally aware and which are associated with any part of
Your Contributions.
5. You represent that each of Your Contributions is Your original creation (see
section 7 for submissions on behalf of others). You represent that Your
Contribution submissions include complete details of any third-party license
or other restriction (including, but not limited to, related patents and
trademarks) of which you are personally aware and which are associated with
any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the
extent You desire to provide support. You may provide support for free, for a

View File

@@ -30,6 +30,18 @@ For example,
}
```
Or, alternatively, if the build process for the plugin is trusted (e.g. it uses [NPM trusted publishing](https://docs.npmjs.com/trusted-publishers)) and the plugin doesn't need to be patched,
1. Add the plugin to `devDependencies` in `package.json`.
2. Update `pluginRepositories.json`, replacing `""` with the plugin's NPM package name:
```json
{
"plugin.id.here": {
"cloneUrl": "https://example.com/plugin-repo/plugin-repo-here.git",
"package": ""
}
}
```
## Patching the plugin
Some plugins need patching. To create or update a plugin's patch, run the `patch-plugin` command in the `packages/default-plugins/` directory.

View File

@@ -10643,6 +10643,7 @@ __metadata:
"@joplin/utils": "npm:~3.5"
"@types/yargs": "npm:17.0.33"
fs-extra: "npm:11.3.2"
joplin-plugin-freehand-drawing: "npm:4.2.0"
ts-node: "npm:10.9.2"
typescript: "npm:5.8.3"
yargs: "npm:17.7.2"
@@ -10986,7 +10987,7 @@ __metadata:
jest-environment-jsdom: "npm:29.7.0"
json-stringify-safe: "npm:5.0.1"
json5: "npm:2.2.3"
katex: "npm:0.16.22"
katex: "npm:0.16.23"
markdown-it: "npm:13.0.2"
markdown-it-abbr: "npm:1.0.4"
markdown-it-anchor: "npm:5.3.0"
@@ -34928,6 +34929,16 @@ __metadata:
languageName: node
linkType: hard
"joplin-plugin-freehand-drawing@npm:4.2.0":
version: 4.2.0
resolution: "joplin-plugin-freehand-drawing@npm:4.2.0"
dependencies:
"@js-draw/material-icons": "npm:1.33.0"
js-draw: "npm:1.33.0"
checksum: 10/457c23b7fbd6f1e3a48568395c1e456d570a3c68e25b40fba97973fd848de1b513be2f1ece6eb9babae862e19ba140aca871eee0e3a8a9cb5a033e0c0fd78934
languageName: node
linkType: hard
"joplin@workspace:packages/app-cli":
version: 0.0.0-use.local
resolution: "joplin@workspace:packages/app-cli"
@@ -35501,14 +35512,14 @@ __metadata:
languageName: node
linkType: hard
"katex@npm:0.16.22":
version: 0.16.22
resolution: "katex@npm:0.16.22"
"katex@npm:0.16.23":
version: 0.16.23
resolution: "katex@npm:0.16.23"
dependencies:
commander: "npm:^8.3.0"
bin:
katex: cli.js
checksum: 10/fdb8667d9aa971154502b120ba340766754d202e3d3e322aca0a96de27032ad2dbb8a7295d798d310cd7ce4ddd21ed1f3318895541b61c9b4fdf611166589e02
checksum: 10/aee350216b80117f65609e933ae1347b0ffd111cda85756de463983a0d874895d2a7c25864e7f7327976e38d96a2c6c3ca07432cabc8b0cdbfecafbe7e155d70
languageName: node
linkType: hard