You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-11 00:21:45 +02:00
Compare commits
29 Commits
cla_update
...
android-v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64f141b28 | ||
|
|
8bba68d920 | ||
|
|
e342f2d572 | ||
|
|
5951a66fef | ||
|
|
04f9bda128 | ||
|
|
7a8a94f557 | ||
|
|
ad000fb521 | ||
|
|
435b896142 | ||
|
|
b12f31c802 | ||
|
|
ddb6d7a677 | ||
|
|
f0a1d05284 | ||
|
|
27f7cb7ca6 | ||
|
|
9e43ebcf43 | ||
|
|
05cc0fa798 | ||
|
|
ee5b631d13 | ||
|
|
e4b6b34d37 | ||
|
|
6f1280f0f5 | ||
|
|
4c9015dab4 | ||
|
|
1adcafce9d | ||
|
|
cc9f55e115 | ||
|
|
e8b3b039df | ||
|
|
d9295a69d1 | ||
|
|
b92743b068 | ||
|
|
03f65a3fb1 | ||
|
|
32a22174f7 | ||
|
|
d154ef4f5c | ||
|
|
b8dd660c28 | ||
|
|
2b20315bf5 | ||
|
|
93b9108832 |
@@ -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
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
48
packages/app-cli/app/command-keymap.ts
Normal file
48
packages/app-cli/app/command-keymap.ts
Normal 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;
|
||||
10
packages/app-cli/tests/md_to_html/abc.html
Normal file
10
packages/app-cli/tests/md_to_html/abc.html
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
<div class="joplin-editable joplin-abc-notation">
|
||||
<pre class="joplin-source" data-abc-options="{"responsive":"resize"}" data-joplin-language="abc" data-joplin-source-open="```abc " data-joplin-source-close=" ``` ">{responsive:'resize'}
|
||||
---
|
||||
K:F
|
||||
!f!(fgag-g2c2)|</pre>
|
||||
<pre class="joplin-rendered joplin-abc-notation-rendered">K:F
|
||||
!f!(fgag-g2c2)|</pre>
|
||||
</div>
|
||||
|
||||
6
packages/app-cli/tests/md_to_html/abc.md
Normal file
6
packages/app-cli/tests/md_to_html/abc.md
Normal file
@@ -0,0 +1,6 @@
|
||||
```abc
|
||||
{ responsive: 'resize' }
|
||||
---
|
||||
K:F
|
||||
!f!(fgag-g2c2)|
|
||||
```
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import buildDefaultPlugins from '../buildDefaultPlugins';
|
||||
|
||||
const buildAll = (outputDirectory: string) => {
|
||||
return buildDefaultPlugins(outputDirectory, {
|
||||
return buildDefaultPlugins({
|
||||
outputParentDir: outputDirectory,
|
||||
beforeInstall: async () => { },
|
||||
beforePatch: async () => { },
|
||||
});
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
8
packages/editor/CodeMirror/theme.ts
vendored
8
packages/editor/CodeMirror/theme.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
24
packages/lib/ObjectUtils.test.ts
Normal file
24
packages/lib/ObjectUtils.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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§ion-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&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>');
|
||||
});
|
||||
|
||||
it('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ name }}</title>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 " data-joplin-source-close=" \`\`\` ">${contentHtml}</pre>
|
||||
<pre class="joplin-source" data-abc-options="${optionsHtml}" data-joplin-language="abc" data-joplin-source-open="\`\`\`abc " data-joplin-source-close=" \`\`\` ">${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
@@ -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",
|
||||
|
||||
@@ -226,3 +226,4 @@ xhdpi
|
||||
xxhdpi
|
||||
xxxhdpi
|
||||
scrollend
|
||||
fgag
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)"
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user