1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-23 23:33:01 +02:00

Compare commits

...

47 Commits

Author SHA1 Message Date
Laurent Cozic
c5598242f9 Android 3.5.4 2025-12-23 21:06:18 +00:00
Laurent Cozic
57980ae916 Lock files and prebuild assets 2025-12-23 19:44:54 +00:00
renovate[bot]
9d1720b6e1 Update dependency sass to v1.93.2 (#13972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 19:41:31 +00:00
Henry Heino
c4e0ed18eb Android: Attempt to fix application hang when opening the camera (#13974) 2025-12-23 19:41:13 +00:00
Henry Heino
150f6c9a3f Android: Fix react-native-vector-icons error when opening a note (#13975) 2025-12-23 19:40:44 +00:00
Henry Heino
6f3781f27a Mobile: Toolbar editor: Fix toolbar editor dismiss button is rendered outside the dialog on small screens (#13976) 2025-12-23 19:40:38 +00:00
Henry Heino
37c3d24650 Chore: Android: Allow disabling the voice typing feature at build time (#13977) 2025-12-23 19:40:29 +00:00
renovate[bot]
bcb3f69d15 Update dependency expo to v53.0.23 (#13968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 13:00:10 +00:00
renovate[bot]
70ffb29af4 Update dependency @playwright/test to v1.55.1 (#13970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 12:59:49 +00:00
Henry Heino
5f61bee712 Mobile: Resolves #520: Viewer, Rich Text Editor: Save/restore the cursor and scroll position when switching notes (#13962) 2025-12-23 11:51:15 +00:00
Henry Heino
496d007f74 Mobile: Rich Text Editor: Fix indent/de-indent buttons do nothing when not in a list (#13961) 2025-12-23 11:50:26 +00:00
renovate[bot]
5a9b389504 Update dependency sass to v1.93.1 (#13969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 04:56:42 +00:00
Henry Heino
107290177e Web: Note viewer: Fix assets from development plugins don't load (#13954) 2025-12-21 10:16:28 +00:00
renovate[bot]
5055c9af3e Update dependency @react-native-documents/picker to v10.1.7 (#13964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 15:30:20 +00:00
renovate[bot]
2ed6650136 Update bitnamilegacy/postgresql Docker tag to v17.6.0 (#13966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 15:30:07 +00:00
Laurent Cozic
e80db6afb5 Server v3.5.2 2025-12-19 21:28:55 +00:00
renovate[bot]
6a06922633 Update dependency @playwright/test to v1.55.0 (#13945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-12-19 17:36:07 +00:00
renovate[bot]
fd02d88739 Update dependency node-gyp to v11.4.2 (#13953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 22:56:07 +00:00
renovate[bot]
dacd460f64 Update dependency node-gyp to v11.4.0 (#13950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 19:41:16 +00:00
Henry Heino
3279485f44 Mobile: Fixes #13081: Rich Text Editor: Fix checklists saved with extra space (#13951) 2025-12-18 19:41:03 +00:00
Henry Heino
eaf8d15be7 Mobile: Rich Text Editor: Set the default math/code block content to the selection (#13952) 2025-12-18 19:40:48 +00:00
Henry Heino
6b186b965a Chore: Fix CI (#13948) 2025-12-18 17:56:09 +00:00
cedecode
7a8ac14c99 All: Translation: Update de_DE.po (#13937) 2025-12-18 12:12:53 -05:00
renovate[bot]
73291fa355 Update dependency mermaid to v11.10.1 (#13930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 02:41:28 +00:00
Henry Heino
27ff8be432 Desktop: OneNote import: Fix certain embedded files are positioned under the header (#13898)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-12-18 01:12:35 +00:00
Linkosred
0904838311 Docs: Add video tutorial link for publish notes documentation (#13902) 2025-12-18 01:12:27 +00:00
Henry Heino
2798cc6027 Mobile: Fixes #13854: Fix some icons are invisible: Upgrade react-native-vector-icons to v12 (#13905) 2025-12-18 01:12:14 +00:00
Linkosred
1ede5bc499 Docs: Add video tutorial link for importing and exporting documentation (#13914) 2025-12-18 01:11:58 +00:00
Henry Heino
418a660a66 Chore: Allow specifying a custom API key at build time (#13917) 2025-12-18 01:11:29 +00:00
Linkosred
5bc073e888 Docs: Add section and video tutorial link about Rich text editor on mobile for Rich text documentation (#13921) 2025-12-18 01:10:50 +00:00
Henry Heino
87b443e051 Mobile: Accessibility: Dark mode: Improve contrast of conflicts notebook title, error messages in "Logs" (#13925) 2025-12-18 01:09:47 +00:00
Henry Heino
8e36644068 Desktop: OneNote importer: Add partial support for importing internal links (#13926) 2025-12-18 01:09:30 +00:00
Henry Heino
1833de789a Desktop: Fix search markers vanish when moving focus to a secondary window (#13927) 2025-12-18 01:09:09 +00:00
Henry Heino
0b18fd988b Desktop: Editor plugins: Fix error logged when pressing enter and a plugin-created input is focused (#13932) 2025-12-18 00:55:34 +00:00
Henry Heino
2ce65b9315 Clipper: Support importing math from Wikipedia and other websites (#13934) 2025-12-18 00:54:58 +00:00
renovate[bot]
8f4f0ee321 Update dependency sharp to v0.34.4 (#13923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 10:25:11 +00:00
renovate[bot]
6a83cc95ee Update dependency mermaid to v11.10.0 (#13929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 10:24:42 +00:00
renovate[bot]
5134b63075 Update dependency esbuild to v0.25.10 (#13924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 21:43:32 +00:00
renovate[bot]
74527d7006 Update dependency dompurify to v3.2.7 (#13922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 19:43:18 +00:00
renovate[bot]
ad909ac6f0 Update dependency @types/serviceworker to v0.0.153 (#13919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 08:55:40 +00:00
summoner
5ff0285b85 ALL: Translation: Update hu_HU.po (#13915) 2025-12-15 13:11:08 -05:00
renovate[bot]
bcb509a965 Update dependency @react-native-documents/picker to v10.1.6 (#13911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 11:00:15 +00:00
renovate[bot]
075c98175e Update dependency fs-extra to v11.3.2 (#13910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 00:48:20 +00:00
Joplin Bot
212112d4b6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-12-14 18:38:22 +00:00
renovate[bot]
74bf0cb655 Update dependency @react-native-community/datetimepicker to v8.4.5 (#13900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 17:46:57 +00:00
Laurent Cozic
b2bdf84f06 Add 'yargs' to renovate.json5 dependencies 2025-12-14 15:36:37 +00:00
Laurent Cozic
a2156a0548 Add yargs-parser to renovate.json5 dependencies 2025-12-14 14:41:48 +00:00
116 changed files with 4091 additions and 966 deletions

View File

@@ -1114,6 +1114,7 @@ packages/editor/CodeMirror/vendor/announceSearchMatch.js
packages/editor/ProseMirror/commands/commands.test.js
packages/editor/ProseMirror/commands/commands.js
packages/editor/ProseMirror/commands/focusEditor.js
packages/editor/ProseMirror/commands/selectDocumentEnd.js
packages/editor/ProseMirror/createEditor.js
packages/editor/ProseMirror/index.js
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
@@ -1146,6 +1147,7 @@ packages/editor/ProseMirror/types.js
packages/editor/ProseMirror/utils/SelectableNodeView.js
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
packages/editor/ProseMirror/utils/clampPointToDocument.js
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
packages/editor/ProseMirror/utils/dom/createButton.js
packages/editor/ProseMirror/utils/dom/createTextArea.js
@@ -1155,6 +1157,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
packages/editor/ProseMirror/utils/forEachHeading.js
packages/editor/ProseMirror/utils/getTextBetween.js
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
packages/editor/ProseMirror/utils/jumpToHash.js
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js

3
.gitignore vendored
View File

@@ -1086,6 +1086,7 @@ packages/editor/CodeMirror/vendor/announceSearchMatch.js
packages/editor/ProseMirror/commands/commands.test.js
packages/editor/ProseMirror/commands/commands.js
packages/editor/ProseMirror/commands/focusEditor.js
packages/editor/ProseMirror/commands/selectDocumentEnd.js
packages/editor/ProseMirror/createEditor.js
packages/editor/ProseMirror/index.js
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
@@ -1118,6 +1119,7 @@ packages/editor/ProseMirror/types.js
packages/editor/ProseMirror/utils/SelectableNodeView.js
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
packages/editor/ProseMirror/utils/clampPointToDocument.js
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
packages/editor/ProseMirror/utils/dom/createButton.js
packages/editor/ProseMirror/utils/dom/createTextArea.js
@@ -1127,6 +1129,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
packages/editor/ProseMirror/utils/forEachHeading.js
packages/editor/ProseMirror/utils/getTextBetween.js
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
packages/editor/ProseMirror/utils/jumpToHash.js
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js

View File

@@ -19,7 +19,7 @@
services:
postgresql-master:
image: 'bitnamilegacy/postgresql:17.5.0'
image: 'bitnamilegacy/postgresql:17.6.0'
ports:
- '5432:5432'
environment:
@@ -36,7 +36,7 @@ services:
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
postgresql-slave:
image: 'bitnamilegacy/postgresql:17.5.0'
image: 'bitnamilegacy/postgresql:17.6.0'
ports:
- '5433:5432'
depends_on:

View File

@@ -81,7 +81,7 @@
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.5",
"execa": "5.1.1",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"glob": "11.0.3",
"gulp": "4.0.2",
"husky": "9.1.7",
@@ -95,7 +95,7 @@
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "11.3.0",
"node-gyp": "11.4.2",
"nodemon": "3.1.10"
},
"packageManager": "yarn@4.9.2",

View File

@@ -107,6 +107,7 @@ class Command extends BaseCommand {
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
username: () => joplinServerAuth.email,
password: () => joplinServerAuth.password,
apiKey: () => '',
session: (): Session => null,
});

View File

@@ -48,7 +48,7 @@
"chalk": "4.1.2",
"compare-version": "0.1.2",
"file-type": "16.5.4",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"html-entities": "1.4.0",
"keytar": "7.9.0",
"md5": "2.3.0",
@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"redux": "4.2.1",
"server-destroy": "1.0.1",
"sharp": "0.34.3",
"sharp": "0.34.4",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",

View File

@@ -0,0 +1,74 @@
<!-- From https://en.wikipedia.org/wiki/Collatz_conjecture -->
<math display="block" xmlns="http://www.w3.org/1998/Math/MathML" alttext="{\displaystyle f(n)={\begin{cases}n/2&amp;{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&amp;{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}">
<semantics>
<mrow class="MJX-TeXAtom-ORD">
<mstyle displaystyle="true" scriptlevel="0">
<mi>f</mi>
<mo stretchy="false">(</mo>
<mi>n</mi>
<mo stretchy="false">)</mo>
<mo>=</mo>
<mrow class="MJX-TeXAtom-ORD">
<mrow>
<mo>{</mo>
<mtable columnalign="left left" rowspacing=".2em" columnspacing="1em" displaystyle="false">
<mtr>
<mtd>
<mi>n</mi>
<mrow class="MJX-TeXAtom-ORD">
<mo>/</mo>
</mrow>
<mn>2</mn>
</mtd>
<mtd>
<mrow class="MJX-TeXAtom-ORD">
<mtext>if&nbsp;</mtext>
</mrow>
<mi>n</mi>
<mo>\u2261</mo>
<mn>0</mn>
<mrow class="MJX-TeXAtom-ORD">
<mspace width="0.444em"></mspace>
<mo stretchy="false">(</mo>
<mi>mod</mi>
<mspace width="0.333em"></mspace>
<mn>2</mn>
<mo stretchy="false">)</mo>
</mrow>
<mo>,</mo>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>3</mn>
<mi>n</mi>
<mo>+</mo>
<mn>1</mn>
</mtd>
<mtd>
<mrow class="MJX-TeXAtom-ORD">
<mtext>if&nbsp;</mtext>
</mrow>
<mi>n</mi>
<mo>\u2261</mo>
<mn>1</mn>
<mrow class="MJX-TeXAtom-ORD">
<mspace width="0.444em"></mspace>
<mo stretchy="false">(</mo>
<mi>mod</mi>
<mspace width="0.333em"></mspace>
<mn>2</mn>
<mo stretchy="false">)</mo>
</mrow>
<mo>.</mo>
</mtd>
</mtr>
</mtable>
<mo fence="true" stretchy="true" symmetric="true"></mo>
</mrow>
</mrow>
</mstyle>
</mrow>
<annotation encoding="application/x-tex">{\displaystyle f(n)={\begin{cases}n/2&amp;{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&amp;{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}</annotation>
</semantics>
</math></span><img src="/some/src/here" class="mwe-math-fallback-image-display mw-invert skin-invert" aria-hidden="true" style="vertical-align: -3.171ex; width:45.735ex; height:7.509ex;"/>

View File

@@ -0,0 +1 @@
${\displaystyle f(n)={\begin{cases}n/2&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}$

File diff suppressed because it is too large Load Diff

View File

@@ -165,6 +165,10 @@
if (a && a.toLowerCase().indexOf('math/tex') >= 0) isVisible = true;
}
if (nodeName === 'annotation') {
if (node.getAttribute('encoding') === 'application/x-tex') isVisible = true;
}
if (nodeName === 'source' && nodeParentName === 'picture') {
isVisible = false;
}

View File

@@ -50,7 +50,7 @@ import WarningBanner from './WarningBanner/WarningBanner';
import UserWebview from '../../services/plugins/UserWebview';
import Logger from '@joplin/utils/Logger';
import usePluginEditorView from './utils/usePluginEditorView';
import { stateUtils } from '@joplin/lib/reducer';
import { defaultWindowId, stateUtils } from '@joplin/lib/reducer';
import { WindowIdContext } from '../NewWindowOrIFrame';
import useResourceUnwatcher from './utils/useResourceUnwatcher';
import StatusBar from './StatusBar';
@@ -722,6 +722,8 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
bodyEditor = 'CodeMirror5';
}
const mainWindowState = stateUtils.windowStateById(state, defaultWindowId);
return {
noteId,
bodyEditor,
@@ -740,7 +742,9 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
customCss: state.customViewerCss,
noteVisiblePanes: windowState.noteVisiblePanes,
watchedResources: windowState.watchedResources,
highlightedWords: state.highlightedWords,
// For now, only the main window has search UI. Show the same search markers in all
// windows:
highlightedWords: mainWindowState.highlightedWords,
plugins: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([

View File

@@ -145,7 +145,7 @@
"@joplin/renderer": "~3.5",
"@joplin/tools": "~3.5",
"@joplin/utils": "~3.5",
"@playwright/test": "1.54.2",
"@playwright/test": "1.55.1",
"@sentry/electron": "4.24.0",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.14",
@@ -209,7 +209,7 @@
"dependencies": {
"@electron/remote": "2.1.3",
"@joplin/onenote-converter": "~3.5",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"keytar": "7.9.0",
"node-fetch": "2.6.7",
"sqlite3": "5.1.6"

View File

@@ -29,12 +29,9 @@ export interface Props {
borderBottom?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
theme?: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onSubmit?: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onDismiss?: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onReady?: Function;
onSubmit?: ()=> void;
onDismiss?: ()=> void;
onReady?: ()=> void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -1,13 +1,14 @@
import { RefObject } from 'react';
import useMessageHandler from './useMessageHandler';
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
export default function(viewRef: RefObject<HTMLIFrameElement>, onSubmit: Function, onDismiss: Function) {
type OnEvent = ()=> void;
export default function(viewRef: RefObject<HTMLIFrameElement>, onSubmit: OnEvent, onDismiss: OnEvent) {
useMessageHandler(viewRef, event => {
const message = event.data?.message;
if (message === 'form-submit') {
if (message === 'form-submit' && onSubmit) {
onSubmit();
} else if (message === 'dismiss') {
} else if (message === 'dismiss' && onDismiss) {
onDismiss();
}
});

View File

@@ -89,8 +89,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097783
versionName "3.5.3"
versionCode 2097784
versionName "3.5.4"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
@@ -145,5 +145,3 @@ dependencies {
implementation jscFlavor
}
}
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

View File

@@ -36,8 +36,5 @@ rootProject.name = 'Joplin'
expoAutolinking.useExpoVersionCatalog()
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':app'
includeBuild(expoAutolinking.reactNativeGradlePlugin)

View File

@@ -5,7 +5,6 @@ import { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle,
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { CameraRef, Props } from './types';
import { _ } from '@joplin/lib/locale';
import { Platform } from 'react-native';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('Camera/expo');
@@ -66,7 +65,9 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
// iOS issue workaround: Since upgrading to Expo SDK 52, closing and reopening the camera on iOS
// never emits onCameraReady. As a workaround, call .resumePreview and wait for it to resolve,
// rather than relying on the CameraView's onCameraReady prop.
if (Platform.OS === 'ios' && camera) {
//
// Update 12/23/2025: This also happens on certain Android devices.
if (camera) {
// Work around an issue on iOS where the onCameraReady callback is never called.
// Instead, wait for the preview to start using resumePreview:
await camera.resumePreview();

View File

@@ -85,7 +85,7 @@ const useStyles = ({ themeId, style, cameraRatio }: UseStyleProps) => {
}, [themeId, style, outputPositioning]);
};
const androidRatios = ['1:1', '4:3', '16:9'];
const androidRatios = ['4:3', '16:9'];
const iOSRatios: string[] = [];
const useAvailableRatios = (): string[] => {
return Platform.OS === 'android' ? androidRatios : iOSRatios;

View File

@@ -67,6 +67,10 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogVaria
alignSelf: 'center',
},
heading: {
// Without flexShrink/flexGrow, the heading can push the close button
// outside of the dialog.
flexShrink: 1,
flexGrow: 1,
},
modalBackground: {
justifyContent: 'center',

View File

@@ -2,10 +2,9 @@
import * as React from 'react';
import { TextStyle, Text, StyleProp } from 'react-native';
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
const AntIcon = require('react-native-vector-icons/AntDesign').default;
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
const Ionicon = require('react-native-vector-icons/Ionicons').default;
import { FontAwesome5 } from '@react-native-vector-icons/fontawesome5';
import { MaterialDesignIcons } from '@react-native-vector-icons/material-design-icons';
import { Ionicons } from '@react-native-vector-icons/ionicons';
interface Props {
name: string;
@@ -43,20 +42,24 @@ const Icon: React.FC<Props> = props => {
};
if (namePrefix.match(/^fa[bsr]?$/)) {
let iconStyle = 'solid';
if (namePrefix.startsWith('fab')) {
iconStyle = 'brand';
} else if (namePrefix.startsWith('fas')) {
iconStyle = 'solid';
}
return (
<FontAwesomeIcon
brand={namePrefix.startsWith('fab')}
solid={namePrefix.startsWith('fas')}
<FontAwesome5
name={nameSuffix}
iconStyle={iconStyle}
{...sharedProps}
/>
);
} else if (namePrefix === 'ant') {
return <AntIcon name={nameSuffix} {...sharedProps}/>;
} else if (namePrefix === 'material') {
return <MaterialCommunityIcon name={nameSuffix} {...sharedProps}/>;
return <MaterialDesignIcons name={nameSuffix} {...sharedProps}/>;
} else if (namePrefix === 'ionicon') {
return <Ionicon name={nameSuffix} {...sharedProps}/>;
return <Ionicons name={nameSuffix} {...sharedProps}/>;
} else if (namePrefix === 'text') {
return (
<Text
@@ -69,7 +72,7 @@ const Icon: React.FC<Props> = props => {
</Text>
);
} else {
return <FontAwesomeIcon name='cog' {...sharedProps}/>;
return <FontAwesome5 name='cog' {...sharedProps}/>;
}
};

View File

@@ -27,7 +27,7 @@ interface WrapperProps {
noteBody: string;
highlightedKeywords?: string[];
noteResources?: Record<string, ResourceInfo>;
onScroll?: (percent: number)=> void;
onScroll?: ()=> void;
onMarkForDownload?: OnMarkForDownloadCallback;
}
@@ -52,7 +52,7 @@ const WrappedNoteViewer: React.FC<WrapperProps> = (
highlightedKeywords={highlightedKeywords}
noteResources={noteResources}
paddingBottom={0}
initialScroll={0}
initialScrollPercent={0}
noteHash={''}
onMarkForDownload={onMarkForDownload}
onScroll={onScroll}

View File

@@ -15,6 +15,7 @@ import CommandService from '@joplin/lib/services/CommandService';
import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import useWebViewSetup from '../../contentScripts/rendererBundle/useWebViewSetup';
import { OnScrollCallback } from '../../contentScripts/rendererBundle/types';
interface Props {
themeId: number;
@@ -25,12 +26,12 @@ interface Props {
highlightedKeywords: string[];
noteResources: Record<string, ResourceInfo>;
paddingBottom: number;
initialScroll: number|null;
initialScrollPercent: number|null;
noteHash: string;
onCheckboxChange?: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback;
onMarkForDownload?: OnMarkForDownloadCallback;
onScroll: (scrollTop: number)=> void;
onScroll: OnScrollCallback;
onLoadEnd?: ()=> void;
pluginStates: PluginStates;
}
@@ -46,9 +47,7 @@ const onJoplinLinkClick = async (message: string) => {
function NoteBodyViewer(props: Props) {
const webviewRef = useRef<WebViewControl>(null);
const onScroll = useCallback(async (scrollTop: number) => {
props.onScroll(scrollTop);
}, [props.onScroll]);
const onScroll = props.onScroll;
const onResourceLongPress = useOnResourceLongPress(
{
@@ -82,7 +81,7 @@ function NoteBodyViewer(props: Props) {
highlightedKeywords: props.highlightedKeywords,
noteResources: props.noteResources,
noteHash: props.noteHash,
initialScroll: props.initialScroll,
initialScrollPercent: props.initialScrollPercent,
paddingBottom: props.paddingBottom,
});

View File

@@ -24,7 +24,7 @@ interface Props {
highlightedKeywords: string[];
noteResources: Record<string, ResourceInfo>;
noteHash: string;
initialScroll: number|undefined;
initialScrollPercent: number|undefined;
paddingBottom: number;
}
@@ -136,7 +136,7 @@ const useRerenderHandler = (props: Props) => {
// If the hash changed, we don't set initial scroll -- we want to scroll to the hash
// instead.
initialScroll: (previousHash && hashChanged) ? undefined : props.initialScroll,
initialScrollPercent: (previousHash && hashChanged) ? undefined : props.initialScrollPercent,
noteHash: props.noteHash,
};

View File

@@ -1,4 +1,4 @@
import { OnMessageEvent } from '../ExtendedWebView/types';
export type OnScrollCallback = (scrollTop: number)=> void;
export { OnScrollCallback } from '../../contentScripts/rendererBundle/types';
export type OnWebViewMessageHandler = (event: OnMessageEvent)=> void;

View File

@@ -28,12 +28,14 @@ const defaultEditorProps = {
globalSearch: '',
noteId: '',
noteHash: '',
initialScroll: 0,
style: {},
toolbarEnabled: true,
readOnly: false,
onChange: ()=>{},
onSelectionChange: ()=>{},
onUndoRedoDepthChange: ()=>{},
onScroll: ()=>{},
onAttach: async ()=>{},
noteResources: {},
plugins: {},

View File

@@ -13,7 +13,7 @@ import { editorFont } from '../global-style';
import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types';
import { EditorControl, EditorSettings, EditorType } from './types';
import { _ } from '@joplin/lib/locale';
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { ChangeEvent, EditorEvent, EditorEventType, EditorScrolledEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
@@ -38,6 +38,7 @@ import Logger from '@joplin/utils/Logger';
const logger = Logger.create('NoteEditor');
type ChangeEventHandler = (event: ChangeEvent)=> void;
type ScrollEventHandler = (event: EditorScrolledEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
type SelectionChangeEventHandler = (event: SelectionRangeChangeEvent)=> void;
type OnAttachCallback = (filePath?: string)=> Promise<void>;
@@ -46,6 +47,7 @@ interface Props {
ref: Ref<EditorControl>;
themeId: number;
initialText: string;
initialScroll: number;
mode: EditorType;
markupLanguage: MarkupLanguage;
noteId: string;
@@ -58,6 +60,7 @@ interface Props {
plugins: PluginStates;
noteResources: ResourceInfos;
onScroll: ScrollEventHandler;
onChange: ChangeEventHandler;
onSelectionChange: SelectionChangeEventHandler;
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
@@ -334,9 +337,11 @@ function NoteEditor(props: Props) {
break;
}
case EditorEventType.Remove:
case EditorEventType.Scroll:
// Not handled
break;
case EditorEventType.Scroll:
props.onScroll(event);
break;
default:
exhaustivenessCheck = event;
return exhaustivenessCheck;
@@ -442,6 +447,7 @@ function NoteEditor(props: Props) {
noteHash={props.noteHash}
initialText={props.initialText}
initialSelection={props.initialSelection}
initialScroll={props.initialScroll}
editorSettings={editorSettings}
globalSearch={props.globalSearch}
onEditorEvent={onEditorEvent}

View File

@@ -215,8 +215,7 @@ describe('RichTextEditor', () => {
firstCheckbox.click();
await waitFor(async () => {
// At present, lists are saved as non-tight lists:
expect(body.trim()).toBe('- [x] Test\n \n- [x] Another test');
expect(body.trim()).toBe('- [x] Test\n- [x] Another test');
});
});
@@ -433,8 +432,14 @@ describe('RichTextEditor', () => {
expect(editor.textContent).toContain('3^2 + 4^2 = 5^2');
});
it('should save lists as single-spaced', async () => {
let body = 'Test:\n\n- this\n- is\n- a\n- test.';
it.each(['-', '- [ ]'])('should save lists as single-spaced (list markers: %j)', async (marker) => {
let body = [
'Test:\n',
'this',
'is',
'a',
'test.',
].join(`\n${marker} `);
render(<WrappedEditor
noteBody={body}
@@ -445,7 +450,13 @@ describe('RichTextEditor', () => {
mockTyping(window, ' Testing');
await waitFor(async () => {
expect(body.trim()).toBe('Test:\n\n- this\n- is\n- a\n- test. Testing');
expect(body.trim()).toBe([
'Test:\n',
'this',
'is',
'a',
'test. Testing',
].join(`\n${marker} `));
});
});

View File

@@ -96,6 +96,8 @@ const RichTextEditor: React.FC<EditorProps> = props => {
themeId: props.themeId,
pluginStates: props.plugins,
noteResources: props.noteResources,
initialSelection: props.initialSelection,
initialScroll: props.initialScroll,
onPostMessage: onPostMessage,
onAttachFile: props.onAttach,
});

View File

@@ -126,12 +126,12 @@ const declarations: CommandDeclaration[] = [
{
name: EditorCommandType.IndentLess,
label: () => _('Decrease indent level'),
iconName: 'ant indent-left',
iconName: 'material format-indent-decrease',
},
{
name: EditorCommandType.IndentMore,
label: () => _('Increase indent level'),
iconName: 'ant indent-right',
iconName: 'material format-indent-increase',
},
{
name: `editor.${EditorCommandType.SwapLineDown}`,

View File

@@ -12,6 +12,7 @@ const defaultWrapperProps: EditorProps = {
noteHash: '',
noteId: '',
initialText: '',
initialScroll: 0,
editorSettings: defaultEditorSettings,
initialSelection: { start: 0, end: 0 },
globalSearch: '',

View File

@@ -62,6 +62,7 @@ export interface EditorProps {
noteHash: string;
initialText: string;
initialSelection: SelectionRange;
initialScroll: number;
editorSettings: EditorSettings;
globalSearch: string;
plugins: PluginStates;

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { PureComponent, ReactElement } from 'react';
import { connect } from 'react-redux';
import { View, Text, StyleSheet, TouchableOpacity, ViewStyle, Platform } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default;
import BackButtonService from '../../services/BackButtonService';
import NavService from '@joplin/lib/services/NavService';
import { _, _n } from '@joplin/lib/locale';
@@ -26,6 +25,7 @@ import WebBetaButton from './WebBetaButton';
import Menu, { MenuOptionType } from './Menu';
import shim from '@joplin/lib/shim';
import CommandService from '@joplin/lib/services/CommandService';
import Icon from '../Icon';
export { MenuOptionType };
// Rather than applying a padding to the whole bar, it is applied to each
@@ -282,7 +282,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
accessibilityHint={_('Show/hide the sidebar')}
accessibilityRole="button">
<View style={styles.sideMenuButton}>
<Icon name="menu" style={styles.topIcon} />
<Icon name="ionicon menu" style={styles.topIcon} accessibilityLabel={null} />
</View>
</TouchableOpacity>
);
@@ -299,8 +299,9 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
accessibilityRole="button">
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
<Icon
name="arrow-back"
name="ionicon arrow-back"
style={styles.topIcon}
accessibilityLabel={null}
/>
</View>
</TouchableOpacity>
@@ -688,7 +689,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
!menuOptions.length || !showContextMenuButton ? null : (
<Menu themeId={this.props.themeId} options={menuOptions}>
<View style={contextMenuStyle} accessibilityLabel={_('Actions')}>
<Icon name="ellipsis-vertical" style={this.styles().contextMenuTrigger} />
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={null}/>
</View>
</Menu>
);

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { useMemo } from 'react';
import { TouchableOpacity, Text, StyleSheet, ScrollView, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
import { themeStyle } from './global-style';
import { AppState } from '../utils/types';

View File

@@ -6,7 +6,7 @@ import { Dispatch } from 'redux';
import { AccessibilityActionEvent, AccessibilityActionInfo, View } from 'react-native';
import { connect } from 'react-redux';
import BottomDrawer from '../BottomDrawer';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
type OnButtonPress = ()=> void;
interface ButtonSpec {

View File

@@ -11,9 +11,9 @@ import { Button } from 'react-native-paper';
import createRootStyle from '../../utils/createRootStyle';
import ScreenHeader from '../ScreenHeader';
import Clipboard from '@react-native-clipboard/clipboard';
const Icon = require('react-native-vector-icons/Ionicons').default;
import Logger from '@joplin/utils/Logger';
import { reg } from '@joplin/lib/registry';
import Icon from '../Icon';
const logger = Logger.create('JoplinCloudLoginScreen');
@@ -179,7 +179,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
</Text>
{state.active === 'LINK_USED' ? (
<Animated.View style={{ transform: [{ rotate: syncIconRotation }] }}>
<Icon name='sync' style={styles.loadingIcon}/>
<Icon name='ionicon sync' style={styles.loadingIcon} accessibilityLabel={_('Waiting for authorisation...')}/>
</Animated.View>
) : null }
</View>

View File

@@ -76,6 +76,8 @@ import { EditorType } from '../../NoteEditor/types';
import { IconButton } from 'react-native-paper';
import { writeTextToCacheFile } from '../../../utils/ShareUtils';
import shareFile from '../../../utils/shareFile';
import NotePositionService from '@joplin/lib/services/NotePositionService';
import VoiceTyping from '../../../services/voiceTyping/VoiceTyping';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const emptyArray: any[] = [];
@@ -154,6 +156,8 @@ interface State {
multiline: boolean;
}
type ScrollEventSlice = { fraction: number };
class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent<State> {
// This isn't in this.state because we don't want changing scroll to trigger
// a re-render.
@@ -226,6 +230,13 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
multiline: false,
};
const initialCursorLocation = NotePositionService.instance().getCursorPosition(props.noteId, defaultWindowId).markdown;
if (initialCursorLocation) {
this.selection = { start: initialCursorLocation, end: initialCursorLocation };
}
const initialScroll = NotePositionService.instance().getScrollPercent(props.noteId, defaultWindowId);
this.lastBodyScroll = initialScroll;
this.titleTextFieldRef = React.createRef();
this.saveActionQueues_ = {};
@@ -770,8 +781,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
this.selection = event.nativeEvent.selection;
};
private onMarkdownEditorSelectionChange = (event: SelectionRangeChangeEvent) => {
private onEditorSelectionChange = (event: SelectionRangeChangeEvent) => {
this.selection = { start: event.from, end: event.to };
NotePositionService.instance().updateCursorPosition(
this.props.noteId, defaultWindowId, { markdown: event.from },
);
};
public makeSaveAction(state: State) {
@@ -1291,8 +1306,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
});
}
const voiceTypingSupported = Platform.OS === 'android';
if (voiceTypingSupported) {
if (VoiceTyping.supported()) {
output.push({
title: _('Voice typing...'),
onPress: () => {
@@ -1487,10 +1501,16 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return this.folderPickerOptions_;
}
private onBodyViewerScroll = (scrollTop: number) => {
this.lastBodyScroll = scrollTop;
private onBodyViewerScroll = (event: ScrollEventSlice) => {
this.lastBodyScroll = event.fraction;
NotePositionService.instance().updateScrollPosition(
this.props.noteId, defaultWindowId, event.fraction,
);
};
private onMarkdownEditorScroll = () => {};
public onBodyViewerCheckboxChange(newBody: string) {
void this.saveOneProperty('body', newBody);
}
@@ -1604,7 +1624,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
onMarkForDownload={this.onMarkForDownload}
onRequestEditResource={this.onEditResource}
onScroll={this.onBodyViewerScroll}
initialScroll={this.lastBodyScroll}
initialScrollPercent={this.lastBodyScroll}
/>
);
} else {
@@ -1658,7 +1678,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
markupLanguage={this.state.note.markup_language}
globalSearch={this.props.searchQuery}
onChange={this.onMarkdownEditorTextChange}
onSelectionChange={this.onMarkdownEditorSelectionChange}
onSelectionChange={this.onEditorSelectionChange}
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
onAttach={this.onAttach}
noteResources={this.state.noteResources}
@@ -1671,6 +1691,14 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
paddingLeft: 0,
paddingRight: 0,
}}
// For now, only save/restore the scroll location for the Rich Text editor since that editor's
// scroll should roughly match the viewer. In the future, it may make sense to refactor this to
// use mapsToLine (similar to what's done on desktop) to sync the Markdown editor scroll, but this
// will require refactoring.
initialScroll={this.props.editorType === EditorType.RichText ? this.lastBodyScroll : undefined}
onScroll={this.props.editorType === EditorType.RichText ? this.onBodyViewerScroll : this.onMarkdownEditorScroll}
mode={this.props.editorType}
/>;
}

View File

@@ -22,6 +22,7 @@ import { themeStyle } from '../global-style';
import getHelpMessage from '@joplin/lib/components/shared/NoteRevisionViewer/getHelpMessage';
import { DialogContext } from '../DialogManager';
import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick';
import { OnScrollCallback } from '../NoteBodyViewer/types';
interface Props {
themeId: number;
@@ -153,6 +154,10 @@ const NoteRevisionViewer: React.FC<Props> = props => {
return result;
}, [revisions]);
const onScroll: OnScrollCallback = useCallback((event) => {
setInitialScroll(event.fraction);
}, []);
const onOptionSelected = useCallback((value: string) => {
setCurrentRevisionId(value);
}, []);
@@ -280,8 +285,8 @@ const NoteRevisionViewer: React.FC<Props> = props => {
noteResources={resources}
highlightedKeywords={emptyStringList}
paddingBottom={0}
initialScroll={initialScroll}
onScroll={setInitialScroll}
initialScrollPercent={initialScroll}
onScroll={onScroll}
noteHash={''}
/>
</View>;

View File

@@ -3,7 +3,6 @@ import { useMemo, useEffect, useCallback, useContext } from 'react';
import { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Image, ImageStyle } from 'react-native';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
const IonIcon = require('react-native-vector-icons/Ionicons').default;
import Icon from './Icon';
import Folder from '@joplin/lib/models/Folder';
import Synchronizer from '@joplin/lib/Synchronizer';
@@ -203,8 +202,8 @@ const FolderItem: React.FC<FolderItemProps> = props => {
const baseStyles = props.styles;
const collapsed = props.collapsed;
const iconName = collapsed ? 'chevron-down' : 'chevron-up';
const iconComp = <IonIcon name={iconName} style={baseStyles.folderToggleIcon} />;
const iconName = collapsed ? 'ionicon chevron-down' : 'ionicon chevron-up';
const iconComp = <Icon name={iconName} style={baseStyles.folderToggleIcon} accessibilityLabel={null} />;
const onTogglePress = useCallback(() => {
props.onTogglePress(props.folder);
@@ -233,7 +232,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
if (folderId === getTrashFolderId()) {
folderIcon = getTrashFolderIcon(FolderIconType.FontAwesome);
} else if (props.alwaysShowFolderIcons) {
return <IonIcon name="folder-outline" style={baseStyles.folderBaseIcon} />;
return <Icon name="ionicon folder-outline" style={baseStyles.folderBaseIcon} accessibilityLabel={null} />;
} else {
return null;
}

View File

@@ -4,7 +4,6 @@ import { Text, Button } from 'react-native-paper';
import { _, languageName } from '@joplin/lib/locale';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import VoiceTyping, { OnTextCallback, VoiceTypingSession } from '../../services/voiceTyping/VoiceTyping';
import whisper from '../../services/voiceTyping/whisper';
import { RecorderState } from './types';
import RecordingControls from './RecordingControls';
import { PrimaryButton } from '../buttons';
@@ -38,7 +37,7 @@ const useVoiceTyping = ({ locale, onSetPreview, onText }: UseVoiceTypingProps) =
voiceTypingRef.current = voiceTyping;
const builder = useMemo(() => {
return new VoiceTyping(locale, [whisper]);
return new VoiceTyping(locale);
}, [locale]);
const [redownloadCounter, setRedownloadCounter] = useState(0);

View File

@@ -10,7 +10,7 @@ const defaultRendererSettings: RenderSettings = {
resources: {},
codeTheme: 'atom-one-light.css',
noteHash: '',
initialScroll: 0,
initialScrollPercent: 0,
readAssetBlob: async (_path: string) => new Blob(),
removeUnusedPluginAssets: true,

View File

@@ -20,7 +20,7 @@ export interface RenderSettings {
resources: ResourceInfos;
codeTheme: string;
noteHash: string;
initialScroll: number;
initialScrollPercent: number;
// If [null], plugin assets are not added to the document
pluginAssetContainerSelector: string|null;
removeUnusedPluginAssets: boolean;

View File

@@ -64,7 +64,10 @@ export const initialize = (options: RendererWebViewOptions) => {
const onMainContentScroll = () => {
const newScrollTop = document.scrollingElement.scrollTop;
if (lastScrollTop !== newScrollTop) {
messenger.remoteApi.onScroll(newScrollTop);
const scrollHeight = document.scrollingElement.scrollHeight;
messenger.remoteApi.onScroll({
fraction: newScrollTop / (scrollHeight || 1),
});
}
};

View File

@@ -19,13 +19,13 @@ const afterFullPageRender = (
}
const hash = renderSettings.noteHash;
const initialScroll = renderSettings.initialScroll;
const initialScrollPercent = renderSettings.initialScrollPercent;
// Don't scroll to a hash if we're given initial scroll (initial scroll
// overrides scrolling to a hash).
if ((initialScroll ?? null) !== null) {
if ((initialScrollPercent ?? null) !== null) {
const scrollingElement = document.scrollingElement ?? document.documentElement;
scrollingElement.scrollTop = initialScroll;
scrollingElement.scrollTop = initialScrollPercent * scrollingElement.scrollHeight;
} else if (hash) {
// Gives it a bit of time before scrolling to the anchor
// so that images are loaded.

View File

@@ -30,20 +30,24 @@ export interface ExtraContentScriptSource {
pluginId: string;
}
export interface ScrollEvent {
fraction: number; // e.g. 0.5 when scrolled 50% of the way through the document
}
export type OnScrollCallback = (scrollTop: ScrollEvent)=> void;
export interface RendererProcessApi {
renderer: Renderer;
jumpToHash: (hash: string)=> void;
}
export interface MainProcessApi {
onScroll(scrollTop: number): void;
onScroll: OnScrollCallback;
onPostMessage(message: string): void;
onPostPluginMessage(contentScriptId: string, message: unknown): Promise<unknown>;
fsDriver: RendererFsDriver;
}
export type OnScrollCallback = (scrollTop: number)=> void;
export interface MarkupRecord {
language: MarkupLanguage;
markup: string;
@@ -62,7 +66,7 @@ export interface RenderOptions {
removeUnusedPluginAssets: boolean;
noteHash: string;
initialScroll: number;
initialScrollPercent: number;
// Forwarded renderer settings
splitted?: boolean;

View File

@@ -12,12 +12,12 @@ import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
import useEditPopup from './utils/useEditPopup';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { RenderSettings } from './contentScript/Renderer';
import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir';
import Resource from '@joplin/lib/models/Resource';
import { ResourceInfos } from '@joplin/renderer/types';
import useContentScripts from './utils/useContentScripts';
import uuid from '@joplin/lib/uuid';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir';
const logger = Logger.create('renderer/useWebViewSetup');
@@ -91,8 +91,8 @@ const useMessenger = (props: UseMessengerProps) => {
const messenger = useMemo(() => {
const fsDriver = shim.fsDriver();
const localApi = {
onScroll: (fraction: number) => onScrollRef.current?.(fraction),
const localApi: MainProcessApi = {
onScroll: (event) => onScrollRef.current?.(event),
onPostMessage: (message: string) => onPostMessageRef.current?.(message),
onPostPluginMessage,
fsDriver: {
@@ -212,20 +212,32 @@ const useWebViewSetup = (props: Props): Result => {
settingsChanged = true;
}
},
readAssetBlob: (assetPath: string): Promise<Blob> => {
// Built-in assets are in resourceDir, external plugin assets are in cacheDir.
const assetsDirs = [Setting.value('resourceDir'), Setting.value('cacheDir')];
// Handles plugin asset loading on web (where the WebView can't load assets directly).
readAssetBlob: async (assetPath: string): Promise<Blob> => {
if (assetPath.startsWith('pluginAssets/')) { // Built-in plugin asset
assetPath = assetPath.replace(/^pluginAssets\//, '');
let resolvedPath = null;
for (const assetDir of assetsDirs) {
resolvedPath ??= resolvePathWithinDir(assetDir, assetPath);
if (resolvedPath) break;
}
const fullPath = shim.fsDriver().resolveRelativePathWithinDir(
Setting.value('pluginAssetDir'), assetPath,
);
return shim.fsDriver().fileAtPath(fullPath);
} else { // Asset from an installed/development plugin
// User-installed plugins are stored in cacheDir
const allowedBasePaths = [Setting.value('cacheDir')];
// Development plugins are stored in other locations. Add them separately:
if (Setting.value('plugins.devPluginPaths')) {
allowedBasePaths.push(...Setting.value('plugins.devPluginPaths').split(','));
}
if (!resolvedPath) {
throw new Error(`Failed to load asset at ${assetPath} -- not in any of the allowed asset directories: ${assetsDirs.join(',')}.`);
for (const basePath of allowedBasePaths) {
const resolved = resolvePathWithinDir(basePath, assetPath);
if (resolved) {
return shim.fsDriver().fileAtPath(resolved);
}
}
throw new Error(`Unable to resolve plugin asset: ${assetPath}`);
}
return shim.fsDriver().fileAtPath(resolvedPath);
},
removeUnusedPluginAssets: options.removeUnusedPluginAssets,
globalSettings: {

View File

@@ -2,26 +2,6 @@
* @jest-environment jsdom
*/
import { writeFileSync } from 'fs-extra';
import { join } from 'path';
import Setting from '@joplin/lib/models/Setting';
// Mock react-native-vector-icons -- it uses ESM imports, which, by default, are not
// supported by jest.
jest.doMock('react-native-vector-icons/Ionicons', () => {
return {
default: {
getImageSourceSync: () => {
// Create an empty file that can be read/used as an image resource.
const iconPath = join(Setting.value('cacheDir'), 'test-icon.png');
writeFileSync(iconPath, '', 'utf-8');
return { uri: iconPath };
},
},
};
});
import lightTheme from '@joplin/lib/themes/light';
import { editPopupClass, getEditPopupSource } from './useEditPopup';
import { describe, it, expect, beforeAll, jest } from '@jest/globals';

View File

@@ -1,42 +1,17 @@
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import { useMemo } from 'react';
import { extname } from 'path';
import shim from '@joplin/lib/shim';
import { Platform } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default;
export const editPopupClass = 'joplin-editPopup';
const getEditIconSrc = (theme: Theme) => {
// Use an inline edit icon on web -- getImageSourceSync isn't supported there.
if (Platform.OS === 'web') {
const svgData = `
<svg viewBox="-103 60 180 180" width="30" height="30" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<path d="m 100,19 c -11.7,0 -21.1,9.5 -21.2,21.2 0,0 42.3,0 42.3,0 0,-11.7 -9.5,-21.2 -21.2,-21.2 z M 79,43 v 143 l 21.3,26.4 21,-26.5 V 42.8 Z" style="transform: rotate(45deg)" fill=${JSON.stringify(theme.color2)}/>
</svg>
`.replace(/[ \t\n]+/, ' ');
return `data:image/svg+xml;utf8,${encodeURIComponent(svgData)}`;
}
const iconUri = Icon.getImageSourceSync('pencil', 20, theme.color2).uri;
// Copy to a location that can be read within a WebView
// (necessary on iOS)
const destPath = `${Setting.value('resourceDir')}/edit-icon${extname(iconUri)}`;
// Copy in the background -- the edit icon popover script doesn't need the
// icon immediately.
void (async () => {
// Can be '' in a testing environment.
if (iconUri) {
await shim.fsDriver().copy(iconUri, destPath);
}
})();
return destPath;
const svgData = `
<svg viewBox="-103 60 180 180" width="30" height="30" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<path d="m 100,19 c -11.7,0 -21.1,9.5 -21.2,21.2 0,0 42.3,0 42.3,0 0,-11.7 -9.5,-21.2 -21.2,-21.2 z M 79,43 v 143 l 21.3,26.4 21,-26.5 V 42.8 Z" style="transform: rotate(45deg)" fill=${JSON.stringify(theme.color2)}/>
</svg>
`.replace(/[ \t\n]+/, ' ');
return `data:image/svg+xml;utf8,${encodeURIComponent(svgData)}`;
};
// Creates JavaScript/CSS that can be used to create an "Edit" button.

View File

@@ -29,6 +29,8 @@ export const initialize = async (
settings,
initialText,
initialNoteId,
initialSelection,
initialScroll,
parentElementClassName,
initialSearch,
}: EditorProps,
@@ -41,6 +43,14 @@ export const initialize = async (
throw new Error('Parent node is not an element.');
}
document.addEventListener('scrollend', () => {
const fraction = document.scrollingElement.scrollTop / (document.scrollingElement.scrollHeight || 1);
void messenger.remoteApi.onEditorEvent({
kind: EditorEventType.Scroll,
fraction,
});
});
const assetContainer = document.createElement('div');
assetContainer.id = 'joplin-container-pluginAssetsContainer';
document.body.appendChild(assetContainer);
@@ -100,6 +110,13 @@ export const initialize = async (
});
editor.setSearchState(initialSearch, 'initialSearch');
if (initialSelection) {
editor.select(initialSelection.start, initialSelection.end);
}
if (initialScroll) {
editor.setScrollPercent(initialScroll);
}
messenger.setLocalInterface({
editor,
});

View File

@@ -3,9 +3,13 @@ import { EditorControl, EditorSettings, OnLocalize, SearchState } from '@joplin/
import { MarkupRecord, RendererControl } from '../rendererBundle/types';
import { RenderResult } from '@joplin/renderer/types';
type SelectionRange = { start: number; end: number };
export interface EditorProps {
initialText: string;
initialSearch: SearchState;
initialSelection: SelectionRange;
initialScroll: number;
initialNoteId: string;
parentElementClassName: string;
settings: EditorSettings;

View File

@@ -14,6 +14,7 @@ import { RendererControl, RenderOptions } from '../rendererBundle/types';
import { ResourceInfos } from '@joplin/renderer/types';
import { _ } from '@joplin/lib/locale';
import { defaultSearchState } from '../../components/NoteEditor/SearchPanel';
import { SelectionRange } from '../markdownEditorBundle/types';
const logger = Logger.create('useWebViewSetup');
@@ -21,6 +22,8 @@ interface Props {
initialText: string;
noteId: string;
settings: EditorSettings;
initialSelection: SelectionRange|null;
initialScroll: number|null;
parentElementClassName: string;
globalSearch: string;
themeId: number;
@@ -53,7 +56,7 @@ const useMessenger = (props: UseMessengerProps) => {
noteViewerFontSize: `${baseTheme.fontSize}${baseTheme.fontSizeUnits ?? 'px'}`,
},
noteHash: '',
initialScroll: 0,
initialScrollPercent: 0,
pluginAssetContainerSelector: null,
removeUnusedPluginAssets: true,
};
@@ -119,6 +122,8 @@ const useSource = (props: UseSourceProps) => {
...defaultSearchState,
searchText: propsRef.current.globalSearch,
},
initialScroll: propsRef.current.initialScroll,
initialSelection: propsRef.current.initialSelection,
settings: propsRef.current.settings,
};

View File

@@ -342,30 +342,17 @@
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/ionicons/fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/material-icons/fonts/MaterialIcons.ttf",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -375,30 +362,17 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialDesignIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;

View File

@@ -42,7 +42,6 @@ target 'Joplin' do
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
post_install do |installer|

View File

@@ -6,7 +6,7 @@ PODS:
- ReactCommon/turbomodule/core
- EXConstants (17.1.7):
- ExpoModulesCore
- Expo (53.0.20):
- Expo (53.0.23):
- DoubleConversion
- ExpoModulesCore
- glog
@@ -1408,7 +1408,7 @@ PODS:
- ReactCommon/turbomodule/core
- react-native-alarm-notification (3.5.0):
- React
- react-native-document-picker (10.1.5):
- react-native-document-picker (10.1.7):
- DoubleConversion
- glog
- hermes-engine
@@ -1520,6 +1520,34 @@ PODS:
- React-Core
- react-native-sqlite-storage (6.0.1):
- React-Core
- react-native-vector-icons (12.3.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-vector-icons-fontawesome5 (12.3.0)
- react-native-vector-icons-ionicons (12.3.0)
- react-native-vector-icons-material-design-icons (12.4.0)
- react-native-vector-icons-material-icons (12.4.0)
- react-native-version-info (1.1.1):
- React-Core
- react-native-webview (13.15.0):
@@ -1874,7 +1902,7 @@ PODS:
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- RNDateTimePicker (8.4.4):
- RNDateTimePicker (8.4.5):
- React-Core
- RNDeviceInfo (14.0.4):
- React-Core
@@ -1916,30 +1944,6 @@ PODS:
- Yoga
- RNSVG (15.13.0):
- React-Core
- RNVectorIcons (10.3.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- SocketRocket (0.7.1)
- Yoga (0.0.0)
- ZXingObjC/Core (3.6.9)
@@ -2012,6 +2016,11 @@ DEPENDENCIES:
- "react-native-saf-x (from `../node_modules/@joplin/react-native-saf-x`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
- "react-native-vector-icons (from `../node_modules/@react-native-vector-icons/get-image`)"
- "react-native-vector-icons-fontawesome5 (from `../node_modules/@react-native-vector-icons/fontawesome5`)"
- "react-native-vector-icons-ionicons (from `../node_modules/@react-native-vector-icons/ionicons`)"
- "react-native-vector-icons-material-design-icons (from `../node_modules/@react-native-vector-icons/material-design-icons`)"
- "react-native-vector-icons-material-icons (from `../node_modules/@react-native-vector-icons/material-icons`)"
- react-native-version-info (from `../node_modules/react-native-version-info`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
@@ -2058,7 +2067,6 @@ DEPENDENCIES:
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
- RNShare (from `../node_modules/react-native-share`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -2191,6 +2199,16 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-sqlite-storage:
:path: "../node_modules/react-native-sqlite-storage"
react-native-vector-icons:
:path: "../node_modules/@react-native-vector-icons/get-image"
react-native-vector-icons-fontawesome5:
:path: "../node_modules/@react-native-vector-icons/fontawesome5"
react-native-vector-icons-ionicons:
:path: "../node_modules/@react-native-vector-icons/ionicons"
react-native-vector-icons-material-design-icons:
:path: "../node_modules/@react-native-vector-icons/material-design-icons"
react-native-vector-icons-material-icons:
:path: "../node_modules/@react-native-vector-icons/material-icons"
react-native-version-info:
:path: "../node_modules/react-native-version-info"
react-native-webview:
@@ -2283,8 +2301,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-share"
RNSVG:
:path: "../node_modules/react-native-svg"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -2293,7 +2309,7 @@ SPEC CHECKSUMS:
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
Expo: b527631da3b11e085809e877b845f9e6cdd68f9c
Expo: c8f323f74218c45c46e27eed40d8a53ba50667c3
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
ExpoCamera: e1879906d41184e84b57d7643119f8509414e318
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
@@ -2340,7 +2356,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
react-native-document-picker: d7580f6e287bbf2c31c071d6b3f252ae1c6586f1
react-native-document-picker: b6419b766863408dacbdf5e97b2f3a694c611150
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-image-picker: 7babe45e727db306b3f00d08c72eda3586d6e9c1
@@ -2351,6 +2367,11 @@ SPEC CHECKSUMS:
react-native-saf-x: 404f0f9a29cc6bf21d88582e054c45a11b28c22b
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-vector-icons: a45ecc326ec090450f152dfc7076ce1173331ce5
react-native-vector-icons-fontawesome5: 271d813e27a86d30bb8cf1fc2f12dae74b74b69b
react-native-vector-icons-ionicons: ad07e944a092a5cf71b8b569d8f5ce2bf674c415
react-native-vector-icons-material-design-icons: 76cd460b3540b80527b4a80fb7f867f7deedb498
react-native-vector-icons-material-icons: d67e485a05560416ff6b5977d5fa7e0eb6af6870
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
@@ -2387,7 +2408,7 @@ SPEC CHECKSUMS:
rn-fetch-blob: 25612b6d6f6e980c6f17ed98ba2f58f5696a51ca
RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
RNDateTimePicker: 8c12d12e8660697c2e176d2f98775764431c141f
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
@@ -2397,11 +2418,10 @@ SPEC CHECKSUMS:
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 40ace3f87cd881869e8085aced9dc16b425c74aa
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
RNVectorIcons: e431ef1e6bef75d6ad0e33a83d376e6207962a9d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 8140bf5e9b1f33537d13122a2fecbacaefb2ee5b
PODFILE CHECKSUM: 862189470c6e7bbee6a39c783bf65a36b631921c
COCOAPODS: 1.16.2

View File

@@ -112,14 +112,19 @@ jest.doMock('@expo/vector-icons/MaterialCommunityIcons', () => {
throw new Error('Not supported in testing environments.');
});
// Used by the renderer
jest.doMock('react-native-vector-icons/Ionicons', () => {
return {
default: class extends require('react-native').View {
static getImageSourceSync = () => ({ uri: '' });
},
};
});
const mockIconLibrary = (libraryName, exportName) => {
jest.doMock(libraryName, () => {
const MockIconComponent = require('react-native').View;
return {
default: MockIconComponent,
[exportName]: MockIconComponent,
};
});
};
mockIconLibrary('@react-native-vector-icons/ionicons', 'Ionicons');
mockIconLibrary('@react-native-vector-icons/material-design-icons', 'MaterialDesignIcons');
mockIconLibrary('@react-native-vector-icons/fontawesome5', 'FontAwesome5');
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
// Use a temporary folder instead.

View File

@@ -30,11 +30,16 @@
"@joplin/utils": "~3.5",
"@js-draw/material-icons": "1.32.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-community/datetimepicker": "8.4.5",
"@react-native-community/geolocation": "3.4.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-documents/picker": "10.1.5",
"@react-native-documents/picker": "10.1.7",
"@react-native-vector-icons/fontawesome5": "12.3.0",
"@react-native-vector-icons/get-image": "12.3.0",
"@react-native-vector-icons/ionicons": "12.3.0",
"@react-native-vector-icons/material-design-icons": "12.4.0",
"@react-native-vector-icons/material-icons": "12.4.0",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
@@ -42,7 +47,7 @@
"crypto-browserify": "3.12.1",
"deprecated-react-native-prop-types": "5.0.0",
"events": "3.3.0",
"expo": "53.0.20",
"expo": "53.0.23",
"expo-av": "15.1.7",
"expo-camera": "16.1.11",
"expo-local-authentication": "16.0.5",
@@ -74,7 +79,6 @@
"react-native-sqlite-storage": "6.0.1",
"react-native-svg": "15.13.0",
"react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.3.0",
"react-native-version-info": "1.1.1",
"react-native-webview": "13.15.0",
"react-native-zip-archive": "7.0.2",
@@ -110,15 +114,15 @@
"@types/node": "18.19.130",
"@types/react": "19.0.14",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.152",
"@types/serviceworker": "0.0.153",
"@types/tar-stream": "3.1.4",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",
"babel-plugin-module-resolver": "4.1.0",
"babel-plugin-react-native-web": "0.21.1",
"esbuild": "0.25.9",
"esbuild": "0.25.10",
"fast-deep-equal": "3.1.3",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
@@ -130,7 +134,7 @@
"react-native-web": "0.21.1",
"react-refresh": "0.17.0",
"react-test-renderer": "19.0.0",
"sharp": "0.34.3",
"sharp": "0.34.4",
"sqlite3": "5.1.6",
"timers-browserify": "2.0.12",
"ts-jest": "29.4.1",

View File

@@ -1,5 +1,5 @@
module.exports = {
hash:"eff3d7e44d37c3c7f09b80c7a51d078b", files: {
hash:"daebd8498ff273c64cf5905d4356e66a", 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":"eff3d7e44d37c3c7f09b80c7a51d078b","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":"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"]}

File diff suppressed because one or more lines are too long

View File

@@ -39,15 +39,17 @@ export interface VoiceTypingProvider {
export default class VoiceTyping {
private provider: VoiceTypingProvider|null = null;
public constructor(
private locale: string,
allProviders: VoiceTypingProvider[],
) {
this.provider = allProviders.find(p => p.supported()) ?? null;
public constructor(private locale: string) {
this.provider = VoiceTyping.providers_.find(p => p.supported()) ?? null;
}
public supported() {
return this.provider !== null;
private static providers_: VoiceTypingProvider[] = [];
public static initialize(providers: VoiceTypingProvider[]) {
this.providers_ = providers;
}
public static supported() {
return this.providers_.some(p => p.supported());
}
private getModelPath() {

View File

@@ -213,7 +213,7 @@ const modelLocalFilepath = () => {
};
const whisper: VoiceTypingProvider = {
supported: () => !!SpeechToTextModule,
supported: () => !!SpeechToTextModule && Setting.value('featureFlag.voiceTypingEnabled'),
modelLocalFilepath: modelLocalFilepath,
getDownloadUrl: (locale) => {
const lang = languageCodeOnly(locale).toLowerCase();

View File

@@ -90,6 +90,8 @@ import PerformanceLogger from '@joplin/lib/PerformanceLogger';
import { Profile } from '@joplin/lib/services/profileConfig/types';
import shim from '@joplin/lib/shim';
import { Platform } from 'react-native';
import VoiceTyping from '../services/voiceTyping/VoiceTyping';
import whisper from '../services/voiceTyping/whisper';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -178,6 +180,9 @@ const buildStartupTasks = (
Setting.setConstant('pluginAssetDir', `${Setting.value('resourceDir')}/pluginAssets`);
Setting.setConstant('pluginDir', `${getProfilesRootDir()}/plugins`);
Setting.setConstant('pluginDataDir', getPluginDataDir(currentProfile, isSubProfile));
Setting.setConstant('sync.9.apiKey', '');
Setting.setConstant('sync.10.apiKey', '');
Setting.setConstant('sync.11.apiKey', '');
});
addTask('buildStartupTasks/make resource directory', async () => {
await shim.fsDriver().mkdir(Setting.value('resourceDir'));
@@ -359,6 +364,9 @@ const buildStartupTasks = (
addTask('buildStartupTasks/migrate PPK', async () => {
await migratePpk();
});
addTask('buildStartupTasks/set up voice typing', async () => {
VoiceTyping.initialize([whisper]);
});
addTask('buildStartupTasks/load folders', async () => {
await refreshFolders(dispatch, '');

View File

@@ -1,29 +1,24 @@
import fontAwesomeFont from 'react-native-vector-icons/Fonts/FontAwesome.ttf';
import fontAwesomeSolidFont from 'react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf';
import fontAwesomeRegularFont from 'react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf';
import fontAwesomeBrandsFont from 'react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf';
import ioniconFont from 'react-native-vector-icons/Fonts/Ionicons.ttf';
import materialCommunityIconsFont from 'react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf';
import antDesignFont from 'react-native-vector-icons/Fonts/AntDesign.ttf';
import fontAwesomeSolidFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf';
import fontAwesomeRegularFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf';
import fontAwesomeBrandsFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf';
import ioniconFont from '@react-native-vector-icons/ionicons/fonts/Ionicons.ttf';
import materialCommunityIconsFont from '@react-native-vector-icons/material-icons/fonts/MaterialIcons.ttf';
import materialIconsFont from '@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf';
// See https://www.npmjs.com/package/react-native-vector-icons
const setUpRnVectorIcons = () => {
const iconFontStyles = `
@font-face {
src: url(${fontAwesomeFont});
font-family: FontAwesome;
}
@font-face {
src: url(${fontAwesomeSolidFont});
font-family: FontAwesome5_Solid;
font-family: FontAwesome5Free-Solid;
}
@font-face {
src: url(${fontAwesomeRegularFont});
font-family: FontAwesome5_Regular;
font-family: FontAwesome5Free-Regular;
}
@font-face {
src: url(${fontAwesomeBrandsFont});
font-family: FontAwesome5_Brands;
font-family: FontAwesome5Brands-Regular;
}
@font-face {
src: url(${ioniconFont});
@@ -34,8 +29,8 @@ const setUpRnVectorIcons = () => {
font-family: MaterialCommunityIcons;
}
@font-face {
src: url(${antDesignFont});
font-family: AntDesign;
src: url(${materialIconsFont});
font-family: MaterialDesignIcons;
}
`;

View File

@@ -79,8 +79,7 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
'@react-native-documents/picker': emptyLibraryMock,
'react-native-exit-app': emptyLibraryMock,
'expo-camera': emptyLibraryMock,
// Remove this after upgrading react-native-vector-icons.
'@react-native-vector-icons/material-design-icons': throwOnLoadLibraryMock,
'react-native-vector-icons/MaterialCommunityIcons': throwOnLoadLibraryMock,
// Workaround for applying serviceworker types to a single file.
// See https://joshuatz.com/posts/2021/strongly-typed-service-workers/.

View File

@@ -19,7 +19,7 @@
},
"dependencies": {
"@joplin/utils": "~3.5",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"yargs": "17.7.2"
}
}

View File

@@ -2,11 +2,16 @@ import { EditorView } from 'prosemirror-view';
import { EditorCommandType } from '../../types';
import commands from './commands';
import createTestEditor from '../testing/createTestEditor';
import selectDocumentEnd from './selectDocumentEnd';
const selectAll = (editor: EditorView) => {
commands[EditorCommandType.SelectAll](editor.state, editor.dispatch, editor);
};
const moveCursorToEnd = (editor: EditorView) => {
selectDocumentEnd(editor.state, editor.dispatch, editor);
};
describe('ProseMirror/commands', () => {
test('textBold should toggle bold formatting', () => {
const editor = createTestEditor({ html: '<p>Test</p>' });
@@ -93,4 +98,50 @@ describe('ProseMirror/commands', () => {
}],
});
});
test.each([
{
label: 'should indent the selected paragraph',
before: '<p>Test</p>',
select: selectAll,
expectedDoc: [
{ type: 'paragraph', content: [{ type: 'text', text: ' Test' }] },
],
},
{
label: 'should not throw an error if the selection is at the end of the document (after the last block)',
before: '<p>Test</p><p>Test 2</p>',
select: moveCursorToEnd,
expectedDoc: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Test' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'Test 2' }] },
{ type: 'paragraph', content: [{ type: 'text', text: ' ' }] },
],
},
])('indentMore should add spaces to the beginning of the selected lines ($label)', ({ before, select, expectedDoc }) => {
const editor = createTestEditor({ html: before });
select(editor);
commands[EditorCommandType.IndentMore](editor.state, editor.dispatch, editor);
expect(editor.state.doc.toJSON()).toMatchObject({
content: expectedDoc,
});
});
test('indentLess should remove spaces from the beginning of the line', () => {
const editor = createTestEditor({ html: '<p> test</p>' });
selectAll(editor);
commands[EditorCommandType.IndentLess](editor.state, editor.dispatch, editor);
expect(editor.state.doc.toJSON()).toMatchObject({
content: [{
content: [
{ type: 'text', text: 'test' },
],
type: 'paragraph',
}],
});
});
});

View File

@@ -4,7 +4,7 @@ import { redo, undo } from 'prosemirror-history';
import { autoJoin, selectAll, setBlockType, toggleMark } from 'prosemirror-commands';
import schema from '../schema';
import { liftListItem, sinkListItem, wrapRangeInList } from 'prosemirror-schema-list';
import { NodeType } from 'prosemirror-model';
import { NodeType, Slice } from 'prosemirror-model';
import { getSearchVisible, setSearchVisible } from '../plugins/searchPlugin';
import { findNext, findPrev, replaceAll, replaceNext } from 'prosemirror-search';
import { getEditorApi } from '../plugins/joplinEditorApiPlugin';
@@ -15,6 +15,7 @@ import jumpToHash from '../utils/jumpToHash';
import focusEditor from './focusEditor';
import canReplaceSelectionWith from '../utils/canReplaceSelectionWith';
import showCreateEditablePrompt from '../plugins/joplinEditablePlugin/showCreateEditablePrompt';
import getTextBetween from '../utils/getTextBetween';
type Dispatch = (tr: Transaction)=> void;
type ExtendedCommand = (state: EditorState, dispatch: Dispatch, view?: EditorView, options?: string[])=> boolean;
@@ -75,6 +76,43 @@ const toggleCode: Command = (state, dispatch, view) => {
return toggleMark(schema.marks.code)(state, dispatch, view) || setBlockType(schema.nodes.paragraph)(state, dispatch, view);
};
const getSelectedBlock = (state: EditorState) => {
const blockRange = state.selection.$from.blockRange(state.selection.$to);
// blockRange can be null in an empty document, or when the selection is after the last
// block (e.g. the very end of the document). Handle this:
const contentStart = blockRange ? blockRange.start + 1 : state.selection.from;
return { blockRange, contentStart };
};
const addTextAtLineStart = (text: string): Command => (state, dispatch) => {
const { contentStart } = getSelectedBlock(state);
let transaction = state.tr;
transaction = transaction.insertText(text, contentStart);
if (dispatch) dispatch(transaction);
return true;
};
const removeTextAtLineStart = (pattern: RegExp): Command => (state, dispatch) => {
const { contentStart, blockRange } = getSelectedBlock(state);
const text = state.doc.textBetween(contentStart, blockRange.end);
const match = text.match(pattern);
if (!match || match.index !== 0) return false;
const contentEnd = contentStart + match[0].length;
// Verify that the indexes are correct. This also helps verify that there aren't any
// non-text nodes (e.g checkboxes) included in the range:
const actualText = state.doc.textBetween(contentStart, contentEnd);
if (actualText) {
const transaction = state.tr.replaceRange(contentStart, contentEnd, Slice.empty);
if (dispatch) dispatch(transaction);
return true;
}
return false;
};
const listItemTypes = [schema.nodes.list_item, schema.nodes.task_list_item];
const commands: Record<EditorCommandType, ExtendedCommand|null> = {
@@ -86,13 +124,20 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
[EditorCommandType.ToggleItalicized]: toggleMark(schema.marks.emphasis),
[EditorCommandType.ToggleCode]: toggleCode,
[EditorCommandType.ToggleMath]: (state, dispatch, view) => {
const selectedText = state.doc.textBetween(state.selection.from, state.selection.to);
const block = selectedText.includes('\n');
const nodeType = block ? schema.nodes.joplinEditableBlock : schema.nodes.joplinEditableInline;
const inlineNodeType = schema.nodes.joplinEditableInline;
const blockNodeType = schema.nodes.joplinEditableBlock;
// If multiple paragraphs are selected, it usually isn't possible to replace them
// to inline math. Fall back to block math:
const block = !canReplaceSelectionWith(state.selection, inlineNodeType);
const nodeType = block ? blockNodeType : inlineNodeType;
if (canReplaceSelectionWith(state.selection, nodeType)) {
if (view) {
return showCreateEditablePrompt(block ? '$$\n\t...\n$$' : '$...$', !block)(state, dispatch, 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);
}
return true;
}
@@ -135,7 +180,10 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
return true;
},
[EditorCommandType.InsertCodeBlock]: (state, dispatch, view) => {
return showCreateEditablePrompt('```\n\n```', false)(state, dispatch, view);
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
return showCreateEditablePrompt(
`\`\`\`\n${selectedText}\n\`\`\``, false,
)(state, dispatch, view);
},
[EditorCommandType.ToggleSearch]: (state, dispatch, view) => {
const command = setSearchVisible(!getSearchVisible(state));
@@ -182,10 +230,12 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
[EditorCommandType.DeleteToLineEnd]: null,
[EditorCommandType.DeleteToLineStart]: null,
[EditorCommandType.IndentMore]: (state, dispatch, view) => {
return listItemTypes.some(type => sinkListItem(type)(state, dispatch, view));
return listItemTypes.some(type => sinkListItem(type)(state, dispatch, view))
|| addTextAtLineStart(' ')(state, dispatch, view);
},
[EditorCommandType.IndentLess]: (state, dispatch, view) => {
return listItemTypes.some(type => liftListItem(type)(state, dispatch, view));
return removeTextAtLineStart(/\s{1,4}/)(state, dispatch, view)
|| listItemTypes.some(type => liftListItem(type)(state, dispatch, view));
},
[EditorCommandType.IndentAuto]: null,
[EditorCommandType.InsertNewlineAndIndent]: null,

View File

@@ -0,0 +1,22 @@
import { Command, TextSelection } from 'prosemirror-state';
const selectDocumentEnd: Command = (state, dispatch) => {
// nodeSize is defined to be the length of the node content plus two (one for the start
// and one for the end token). However, the main document has no start/end tokens, so subtract two.
const position = state.doc.nodeSize - 2;
const endAlreadySelected = position === state.selection.from && state.selection.to === state.selection.from;
if (endAlreadySelected) {
return false;
}
const transaction = state.tr.setSelection(TextSelection.create(state.doc, position));
if (dispatch) {
dispatch(transaction);
}
return true;
};
export default selectDocumentEnd;

View File

@@ -28,6 +28,7 @@ import { RenderResult } from '../../renderer/types';
import postprocessEditorOutput from './utils/postprocessEditorOutput';
import detailsPlugin from './plugins/detailsPlugin';
import tablePlugin from './plugins/tablePlugin';
import clampPointToDocument from './utils/clampPointToDocument';
interface ProseMirrorControl extends EditorControl {
getSettings(): EditorSettings;
@@ -134,6 +135,14 @@ const createEditor = async (
formatting: selectionFormatting,
});
}
props.onEvent({
kind: EditorEventType.SelectionRangeChange,
anchor: newState.selection.anchor,
head: newState.selection.head,
from: newState.selection.from,
to: newState.selection.to,
});
};
const view = new EditorView(parentElement, {
@@ -187,10 +196,14 @@ const createEditor = async (
redo: () => {
void editorControl.execCommand(EditorCommandType.Redo);
},
select: function(anchor: number, head: number): void {
select: (anchor: number, head: number) => {
const transaction = view.state.tr;
transaction.setSelection(
TextSelection.create(transaction.doc, anchor, head),
TextSelection.create(
transaction.doc,
clampPointToDocument(view.state, anchor),
clampPointToDocument(view.state, head),
),
);
view.dispatch(transaction);
},

View File

@@ -161,6 +161,7 @@ const nodes = addDefaultToplevelAttributes({
inline: true,
group: 'inlineBreak',
selectable: false,
leafText: () => '\n',
parseDOM: [{ tag: 'br' }],
toDOM: () => domOutputSpecs.br,
},

View File

@@ -0,0 +1,19 @@
import { EditorState } from 'prosemirror-state';
const documentMaximumIndex = (state: EditorState) => {
// nodeSize is documented to be the size of a node's content plus two (one for the
// start marker and one for the end marker). The main document doesn't have start or
// end markers, so subtract these to get the document maximum index:
return state.doc.nodeSize - 2;
};
const clampPointToDocument = (state: EditorState, point: number) => {
if (point < 0) return 0;
const maximumIndex = documentMaximumIndex(state);
if (point > maximumIndex) return maximumIndex;
return point;
};
export default clampPointToDocument;

View File

@@ -0,0 +1,8 @@
import { Node } from 'prosemirror-model';
const getTextBetween = (doc: Node, from: number, to: number) => {
const blockSeparator = '\n\n';
return doc.textBetween(from, to, blockSeparator);
};
export default getTextBetween;

View File

@@ -31,4 +31,30 @@ describe('postprocessEditorOutput', () => {
`),
);
});
// Removing extra space around checklist item content prevents extra space from being
// added when converting from HTML to Markdown
test('should remove wrapper paragraphs from around checklist items', () => {
const doc = new DOMParser().parseFromString(`
<body>
<ul>
<li><input><div><p>Should remove single wrapper paragraphs to avoid extra newlines when saving as Markdown.</p></div></li>
<li><input><div><p>Should not remove paragraphs...</p><p>...when there are multiple.</p></div></li>
</ul>
</body>
`, 'text/html');
const output = postprocessEditorOutput(doc.body);
expect(
normalizeHtmlString(output.querySelector('ul').outerHTML),
).toBe(
normalizeHtmlString(`
<ul>
<li><input><span>Should remove single wrapper paragraphs to avoid extra newlines when saving as Markdown.</span></li>
<li><input><div><p>Should not remove paragraphs...</p><p>...when there are multiple.</p></div></li>
</ul>
`),
);
});
});

View File

@@ -13,6 +13,7 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
for (const item of listItems) {
trimEmptyParagraphs(item);
// Replace <li><p>...text...</p></li> with <li>...text...</li>
if (item.children.length === 1) {
const firstChild = item.children[0];
if (firstChild.tagName === 'P') {
@@ -22,6 +23,30 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
}
};
// Avoids extra newlines from being included in the output Markdown
const removeChecklistItemWrapperParagraphs = (container: HTMLElement) => {
const listItems = container.querySelectorAll<HTMLLIElement>('li');
for (const item of listItems) {
// Is it a checklist item?
if (item.children.length !== 2) continue;
const input = item.children[0];
const content = item.children[1];
if (input.tagName !== 'INPUT' || content.tagName !== 'DIV') continue;
trimEmptyParagraphs(content);
// Replace <li><input/><div><p>...text...</p></div></li> with <li><input/><span>...text...</span></li>
if (content.children.length === 1) {
const firstChild = content.children[0];
if (firstChild.tagName === 'P') {
const newContent = document.createElement('span');
newContent.replaceChildren(...firstChild.childNodes);
content.replaceWith(newContent);
}
}
}
};
const restoreOriginalLinks = (container: HTMLElement) => {
// Restore HREFs
const links = container.querySelectorAll<HTMLAnchorElement>('a[href="#"][data-original-href]');
@@ -63,6 +88,7 @@ const postprocessEditorOutput = (node: Node|DocumentFragment) => {
fixResourceUrls(html);
restoreOriginalLinks(html);
removeListItemWrapperParagraphs(html);
removeChecklistItemWrapperParagraphs(html);
removeTableItemExtraPadding(html);
return html;

View File

@@ -44,7 +44,7 @@
"@lezer/highlight": "1.2.1",
"@lezer/markdown": "1.3.2",
"@replit/codemirror-vim": "6.2.1",
"dompurify": "3.2.6",
"dompurify": "3.2.7",
"orderedmap": "2.1.1",
"prosemirror-commands": "1.7.1",
"prosemirror-dropcursor": "1.8.2",

View File

@@ -28,7 +28,7 @@
"@adobe/css-tools": "4.4.4",
"@joplin/fork-htmlparser2": "^4.1.60",
"datauri": "4.1.0",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"html-entities": "1.4.0"
},
"devDependencies": {

View File

@@ -16,6 +16,7 @@ interface Options {
userContentBaseUrl(): string;
username(): string;
password(): string;
apiKey(): string;
session(): Session | null;
env?: Env;
}
@@ -43,7 +44,6 @@ export interface Session {
}
export default class JoplinServerApi {
private options_: Options;
private session_: Session;
private debugRequests_ = false;
@@ -96,6 +96,7 @@ export default class JoplinServerApi {
this.session_ = await this.exec_('POST', 'api/sessions', null, {
email: this.options_.username(),
password: this.options_.password(),
apiKey: this.options_.apiKey(),
...clientInfo,
});

View File

@@ -10,6 +10,7 @@ interface FileApiOptions {
userContentPath(): string;
username(): string;
password(): string;
apiKey(): string;
}
export default class SyncTargetJoplinCloud extends BaseSyncTarget {
@@ -89,6 +90,7 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
userContentPath: () => Setting.value('sync.10.userContentPath'),
username: () => Setting.value('sync.10.username'),
password: () => Setting.value('sync.10.password'),
apiKey: () => Setting.value('sync.10.apiKey'),
});
}

View File

@@ -14,6 +14,7 @@ export interface FileApiOptions {
userContentPath(): string;
username(): string;
password(): string;
apiKey(): string;
}
export async function newFileApi(id: number, options: FileApiOptions) {
@@ -22,6 +23,7 @@ export async function newFileApi(id: number, options: FileApiOptions) {
userContentBaseUrl: () => options.userContentPath(),
username: () => options.username(),
password: () => options.password(),
apiKey: () => options.apiKey(),
session: (): Session => null,
env: Setting.value('env'),
};
@@ -148,6 +150,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
userContentPath: () => Setting.value('sync.9.userContentPath'),
username: () => Setting.value('sync.9.username'),
password: () => Setting.value('sync.9.password'),
apiKey: () => Setting.value('sync.9.apiKey'),
});
}

View File

@@ -12,6 +12,7 @@ export async function newFileApi(id: number, options: FileApiOptions) {
userContentBaseUrl: () => options.userContentPath(),
username: () => '',
password: () => '',
apiKey: () => Setting.value('sync.11.apiKey'),
session: () => ({ id: Setting.value('sync.11.id'), user_id: Setting.value('sync.11.userId') }),
env: Setting.value('env'),
};
@@ -128,6 +129,7 @@ export default class SyncTargetJoplinServerSAML extends SyncTargetJoplinServer {
userContentPath: () => Setting.value('sync.11.userContentPath'),
username: () => '',
password: () => '',
apiKey: () => Setting.value('sync.11.apiKey'),
};
return initFileApi(SyncTargetJoplinServerSAML.id(), this.logger(), this.lastFileApiOptions_);
}

View File

@@ -61,6 +61,10 @@ export interface Constants {
syncVersion: number;
startupDevPlugins: string[];
isSubProfile: boolean;
'sync.9.apiKey': string;
'sync.10.apiKey': string;
'sync.11.apiKey': string;
}
interface SettingSections {
@@ -296,6 +300,10 @@ class Setting extends BaseModel {
syncVersion: 3,
startupDevPlugins: [],
isSubProfile: false,
'sync.9.apiKey': '',
'sync.10.apiKey': '',
'sync.11.apiKey': '',
};
public static autoSaveEnabled = true;

View File

@@ -1907,6 +1907,19 @@ const builtInMetadata = (Setting: typeof SettingType) => {
isGlobal: true,
},
// As of December 2025, the voice typing feature doesn't work well on low-resource devices.
// There have been requests to allow disabling the voice typing feature at build time. This
// feature flag allows doing so, by changing the default `value` from `true` to `false`:
'featureFlag.voiceTypingEnabled': {
value: true,
type: SettingItemType.Bool,
public: false,
appTypes: [AppType.Mobile],
label: () => 'Voice typing: Enable the voice typing feature',
section: 'note',
advanced: true,
},
'survey.webClientEval2025.progress': {
value: SurveyProgress.NotStarted,
type: SettingItemType.Int,

View File

@@ -35,7 +35,7 @@
"pdfjs-dist": "3.11.174",
"react": "18.3.1",
"react-test-renderer": "18.3.1",
"sharp": "0.34.3",
"sharp": "0.34.4",
"tesseract.js": "6.0.1",
"typescript": "5.8.3"
},
@@ -66,7 +66,7 @@
"file-type": "16.5.4",
"follow-redirects": "1.15.11",
"form-data": "4.0.4",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"hpagent": "1.2.0",
"html-entities": "1.4.0",
"html-minifier": "4.0.0",

View File

@@ -91,6 +91,8 @@ export interface WindowState {
selectedItemType: string;
selectedSmartFilterId: string;
highlightedWords: string[];
backwardHistoryNotes: NoteEntity[];
forwardHistoryNotes: NoteEntity[];
lastSelectedNotesIds: StateLastSelectedNotesIds;
@@ -113,6 +115,7 @@ export const defaultWindowState: WindowState = {
selectedSmartFilterId: null,
selectedItemType: 'note',
selectedNoteTags: [],
highlightedWords: [],
backwardHistoryNotes: [],
forwardHistoryNotes: [],
lastSelectedNotesIds: {
@@ -138,7 +141,6 @@ export interface State extends WindowState {
notLoadedMasterKeys: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
searches: any[];
highlightedWords: string[];
showSideMenu: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
screens: any;

View File

@@ -185,16 +185,21 @@ describe('InteropService_Importer_OneNote', () => {
const content = await readFile(filepath, 'utf-8');
const jsdom = new JSDOM('<div></div>');
InteropService.instance().domParser = new jsdom.window.DOMParser();
InteropService.instance().xmlSerializer = new jsdom.window.XMLSerializer();
const domParser = new jsdom.window.DOMParser();
const xmlSerializer = new jsdom.window.XMLSerializer();
const importer = new InteropService_Importer_OneNote();
await importer.init('asdf', {
domParser: new jsdom.window.DOMParser(),
xmlSerializer: new jsdom.window.XMLSerializer(),
domParser,
xmlSerializer,
});
expectWithInstructions(importer.extractSvgs(content, titleGenerator())).toMatchSnapshot();
const dom = domParser.parseFromString(content, 'text/html');
const extracted = importer.extractSvgs(dom, titleGenerator());
expect(extracted).toMatchObject({ changed: true });
expectWithInstructions(
{ html: dom.body.outerHTML, svgs: extracted.svgs },
).toMatchSnapshot();
});
it('should ignore broken characters at the start of paragraph', async () => {
@@ -317,4 +322,11 @@ describe('InteropService_Importer_OneNote', () => {
expect(markdown).toMatchSnapshot('Math');
});
it('should apply position data for embedded files', async () => {
const notes = await importNote(`${supportDir}/onenote/testOneNoteEmbeddedWordDoc.one`);
const importedNote = notes.find(n => n.title.startsWith('Embedded doc sheet'));
expect(normalizeNoteForSnapshot(importedNote.body)).toMatchSnapshot('EmbeddedFiles');
});
});

View File

@@ -4,7 +4,7 @@ import InteropService_Importer_Base from './InteropService_Importer_Base';
import { NoteEntity } from '../database/types';
import { rtrimSlashes } from '../../path-utils';
import InteropService_Importer_Md from './InteropService_Importer_Md';
import { join, resolve, normalize, sep, dirname, extname, basename } from 'path';
import { join, resolve, normalize, sep, dirname, extname, basename, relative } from 'path';
import Logger from '@joplin/utils/Logger';
import { uuidgen } from '../../uuid';
import shim from '../../shim';
@@ -16,9 +16,9 @@ export type SvgXml = {
content: string;
};
type ExtractSvgsReturn = {
svgs: SvgXml[];
html: string;
type PageResolutionResult = { path: string };
type PageIdMap = {
get: (pageId: string)=> PageResolutionResult|null;
};
// See onenote-converter README.md for more information
@@ -117,8 +117,8 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
this.options_.onError?.(new Error(`None of the files appear to be from OneNote. Skipped files include: ${JSON.stringify(skippedFiles)}`));
}
logger.info('Extracting SVGs into files');
await this.moveSvgToLocalFile(tempOutputDirectory);
logger.info('Postprocessing imported content...');
await this.postprocessGeneratedHtml_(tempOutputDirectory);
logger.info('Importing HTML into Joplin');
const importer = new InteropService_Importer_Md();
@@ -144,41 +144,113 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
}
}
private async moveSvgToLocalFile(baseFolder: string) {
const htmlFiles = await this.getValidHtmlFiles(resolve(baseFolder));
private async buildIdMap_(baseFolder: string): Promise<PageIdMap> {
const htmlFiles = await this.getValidHtmlFiles_(resolve(baseFolder));
const pageIdToPath = new Map<string, string>();
for (const file of htmlFiles) {
const fullPath = join(baseFolder, file.path);
const html: string = await shim.fsDriver().readFile(fullPath);
const metaTagMatch = html.match(/<meta name="X-Original-Page-Id" content="([^"]+)"/i);
if (metaTagMatch) {
const pageId = metaTagMatch[1];
pageIdToPath.set(pageId.toUpperCase(), fullPath);
}
}
return {
get: (id: string)=>{
const path = pageIdToPath.get(id.toUpperCase());
if (path) {
return { path };
}
return null;
},
};
}
private async postprocessGeneratedHtml_(baseFolder: string) {
const htmlFiles = await this.getValidHtmlFiles_(resolve(baseFolder));
const pipeline = [
(dom: Document, currentFolder: string) => this.extractSvgsToFiles_(dom, currentFolder),
(dom: Document, currentFolder: string) => this.convertExternalLinksToInternalLinks_(dom, currentFolder),
];
for (const file of htmlFiles) {
const fileLocation = join(baseFolder, file.path);
const originalHtml = await shim.fsDriver().readFile(fileLocation);
const { svgs, html: updatedHtml } = this.extractSvgs(originalHtml, () => uuidgen(10));
const dom = this.domParser.parseFromString(originalHtml, 'text/html');
if (!svgs || !svgs.length) continue;
let changed = false;
for (const task of pipeline) {
const result = await task(dom, dirname(fileLocation));
changed ||= result;
}
await shim.fsDriver().writeFile(fileLocation, updatedHtml, 'utf8');
await this.createSvgFiles(svgs, join(baseFolder, dirname(file.path)));
if (changed) {
// Don't use xmlSerializer here: It breaks <style> blocks.
const updatedHtml = `<!DOCTYPE HTML>\n${dom.documentElement.outerHTML}`;
await shim.fsDriver().writeFile(fileLocation, updatedHtml, 'utf-8');
}
}
}
private async getValidHtmlFiles(baseFolder: string) {
private async getValidHtmlFiles_(baseFolder: string) {
const files = await shim.fsDriver().readDirStats(baseFolder, { recursive: true });
const htmlFiles = files.filter(f => !f.isDirectory() && f.path.endsWith('.html'));
return htmlFiles;
}
private async createSvgFiles(svgs: SvgXml[], svgBaseFolder: string) {
private async convertExternalLinksToInternalLinks_(dom: Document, baseFolder: string) {
let idMap_: PageIdMap|null = null;
const idMap = async () => {
idMap_ ??= await this.buildIdMap_(baseFolder);
return idMap_;
};
const links = dom.querySelectorAll<HTMLAnchorElement>('a[href^="onenote"]');
let changed = false;
for (const link of links) {
if (!link.href.startsWith('onenote:')) continue;
// Remove everything before the first query parameter (e.g. &section-id=).
const separatorIndex = link.href.indexOf('&');
const prefixRemoved = link.href.substring(separatorIndex);
const params = new URLSearchParams(prefixRemoved);
const pageId = params.get('page-id');
const targetPage = (await idMap()).get(pageId);
// The target page might be in a different notebook (imported separately)
if (!targetPage) {
logger.info('Page not found for internal link. Page ID: ', pageId);
} else {
changed = true;
link.href = relative(baseFolder, targetPage.path);
}
}
return changed;
}
private async extractSvgsToFiles_(dom: Document, svgBaseFolder: string) {
const { svgs, changed } = this.extractSvgs(dom);
for (const svg of svgs) {
await shim.fsDriver().writeFile(join(svgBaseFolder, svg.title), svg.content, 'utf8');
}
return changed;
}
public extractSvgs(html: string, titleGenerator: ()=> string): ExtractSvgsReturn {
const dom = this.domParser.parseFromString(html, 'text/html');
// Public to allow testing:
public extractSvgs(dom: Document, titleGenerator: ()=> string = () => uuidgen(10)) {
// get all "top-level" SVGS (ignore nested)
const svgNodeList = dom.querySelectorAll('svg');
if (!svgNodeList || !svgNodeList.length) {
return { svgs: [], html };
return { svgs: [], changed: false };
}
const svgs: SvgXml[] = [];
@@ -220,8 +292,7 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
return {
svgs,
// Don't use xmlSerializer here: It breaks <style> blocks.
html: `<!DOCTYPE HTML>\n${dom.documentElement.outerHTML}`,
changed: true,
};
}
}

View File

@@ -1,5 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InteropService_Importer_OneNote should apply position data for embedded files: EmbeddedFiles 1`] = `
"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Embedded doc sheet</title>
<meta name="X-Original-Page-Id" content="{C7F7C2C8-DFFA-4AD4-B5E3-84A969D31C8C}"/>
<style>
/* (For testing: Removed default CSS) */
</style>
</head>
<body>
<div class="title" style="left: 48px; position: absolute; top: 24px;"><div class="container-outline"><div class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri Light; font-size: 20pt; line-height: 32px;">Embedded doc sheet</span></div>
</div><div class="container-outline"><div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(118,118,118); font-family: Calibri; font-size: 10pt; line-height: 16px;">Wednesday, December 11, 2019</span></div>
<div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(118,118,118); font-family: Calibri; font-size: 10pt; line-height: 16px;">5:35 PM</span></div>
</div></div><p style="font-size: 11pt; line-height: 17px; margin-left: 2in; margin-top: 1.7in;"><a href=":/id-here">Dude this is a super cool embedded doc.docx</a></p><div class="container-outline" style="left: 192px; position: absolute; top: 259px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; line-height: 17px;">&nbsp;</p></div>
</div>
<script>
if (window.parent !== null) {
window.parent.postMessage(window.location.href, '*');
}
</script>
</body>
</html>"
`;
exports[`InteropService_Importer_OneNote should be able to create notes from corrupted attachment: new_section 1`] = `
"<!DOCTYPE html>
<html lang="en">
@@ -104,6 +136,7 @@ exports[`InteropService_Importer_OneNote should be able to create notes from cor
<head>
<meta charset="UTF-8">
<title>title</title>
<meta name="X-Original-Page-Id" content="{B8EACEFB-CA13-42D8-AECA-5F277CE28041}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -130,7 +163,7 @@ exports[`InteropService_Importer_OneNote should be able to create notes from cor
`;
exports[`InteropService_Importer_OneNote should correctly convert imported notes to Markdown: Test Todo: As Markdown 1`] = `
" Test Todo : cases à cocher lien vers doc sur partage
" Test Todo : cases à cocher lien vers doc sur partage
Test Todo : cases à cocher lien vers doc sur partage
@@ -156,7 +189,7 @@ jeudi 23 octobre 2025
`;
exports[`InteropService_Importer_OneNote should correctly import math formulas: Math 1`] = `
" Math
" Math
Math
@@ -198,6 +231,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
<html lang="en"><head>
<meta charset="UTF-8">
<title>A page can have any width it wants?</title>
<meta name="X-Original-Page-Id" content="{CB4D5A97-3C6D-4DB7-A56A-B5A8B9D85391}">
<style>
/* (For testing: Removed default CSS) */
@@ -230,6 +264,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
<html lang="en"><head>
<meta charset="UTF-8">
<title>A page with a lot of svgs</title>
<meta name="X-Original-Page-Id" content="{2F308E45-ADB4-47EF-9350-D0F7A18AEC0A}">
<style>
/* (For testing: Removed default CSS) */
@@ -260,6 +295,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
<html lang="en"><head>
<meta charset="UTF-8">
<title>A page with text and drawing above it</title>
<meta name="X-Original-Page-Id" content="{6ABD6E20-4015-42B8-953E-74E620C82471}">
<style>
/* (For testing: Removed default CSS) */
@@ -296,6 +332,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
<html lang="en"><head>
<meta charset="UTF-8">
<title>A simple filename</title>
<meta name="X-Original-Page-Id" content="{D7A26766-0233-4170-9EF3-372379551FF7}">
<style>
/* (For testing: Removed default CSS) */
@@ -326,6 +363,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
<html lang="en"><head>
<meta charset="UTF-8">
<title>Page with more than one font size</title>
<meta name="X-Original-Page-Id" content="{074A237A-AFFA-4C2E-907E-48E82E80809D}">
<style>
/* (For testing: Removed default CSS) */
@@ -467,6 +505,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
<html lang="en"><head>
<meta charset="UTF-8">
<title>text</title>
<meta name="X-Original-Page-Id" content="{55390DC0-8B2B-4A1E-9795-DA8033492F7C}">
<style>
/* (For testing: Removed default CSS) */
@@ -498,14 +537,13 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
exports[`InteropService_Importer_OneNote should extract svgs 1`] = `
{
"html": "<!DOCTYPE HTML>
<html lang="en"><head></head><body>
"html": "<body>
<div class="container-outline" style="left: 48px;position: absolute;top: 107px;width: 624px;">
<img src="./id1.svg">
</div>
</body></html>",
</body>",
"svgs": [
{
"content": "<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
@@ -536,32 +574,7 @@ exports[`InteropService_Importer_OneNote should extract svgs 1`] = `
exports[`InteropService_Importer_OneNote should extract svgs 2`] = `
{
"html": "<!DOCTYPE HTML>
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Created on OneNote App</title>
<style>
* { margin: 0; padding: 0; font-weight: normal; }
table, tr, td { border-color: #A3A3A3; }
ul, ol { padding: 0; }
.title .outline-element { display: inline; }
.title .outline-element:nth-child(2) { margin-left: 10px !important; }
.container-outline { font-family: Calibri, sans-serif; font-size: 6pt; }
.ink-text, .ink-space { display: inline-block; position: relative; vertical-align: bottom; }
.ink-text { top: 0; left: 0; }
.note-tag-icon { position: relative; }
.note-tag-icon > svg { position: absolute; }
.icon-secondary > svg { position: absolute; fill: black; filter: drop-shadow(0 0 2px white); height: 12px; top: -1px; }
.icon-secondary > .content { position: absolute; color: black; filter: drop-shadow(0 0 2px white); font-size: 10px; color: black; top: -1px; user-select: none; }
.list-0 li { padding-left: 10px; }
.list-0 li::marker { content: '•'; font-family: Calibri; font-size: 12pt; }
.list-1 li { padding-left: 10px; }
.list-1 li::marker { content: '○'; font-family: Courier New; font-size: 12pt; }
</style>
</head>
<body>
"html": "<body>
@@ -577,7 +590,7 @@ exports[`InteropService_Importer_OneNote should extract svgs 2`] = `
</script>
</body></html>",
</body>",
"svgs": [
{
"content": "<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 0 295 1550"><path d="M 70 1454 l 4 0 6 -14 6 -31 4 -53 4 -69 6 -73 7 -81 6 -90 5 -101 12 -116 14 -123 12 -113 10 -103 6 -95 8 -82 8 -72 6 -58 5 -47 3 -37 1 -26" fill="none" opacity="1.00" stroke="rgb(64, 64, 64)" stroke-linecap="round" stroke-linejoin="round" stroke-width="70"/></svg>",
@@ -809,6 +822,7 @@ exports[`InteropService_Importer_OneNote should ignore broken characters at the
<head>
<meta charset="UTF-8">
<title>Action research - Wikipedia</title>
<meta name="X-Original-Page-Id" content="{5B2B7262-DC9F-47D5-A64D-9BB61513B52E}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -872,6 +886,7 @@ exports[`InteropService_Importer_OneNote should import a simple OneNote notebook
<head>
<meta charset="UTF-8">
<title>Page title</title>
<meta name="X-Original-Page-Id" content="{D7355639-C72B-494D-9B70-774A0060FD10}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -1007,6 +1022,7 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: Tip
<head>
<meta charset="UTF-8">
<title>Tips from a Pro: Using Trees for Dramatic Landscape Photography</title>
<meta name="X-Original-Page-Id" content="{B4FB39E6-163B-472A-9330-A37F97DCF590}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -1038,6 +1054,7 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: wik
<head>
<meta charset="UTF-8">
<title>wikipedia link</title>
<meta name="X-Original-Page-Id" content="{542DF835-4AE2-4231-A924-98D4B8D4F95C}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -1064,11 +1081,11 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: wik
`;
exports[`InteropService_Importer_OneNote should remove hyperlink from title: 风景 (Web view) 1`] = `
"<!DOCTYPE html>
<html lang="en">
<head>
"<!DOCTYPE HTML>
<html lang="en"><head>
<meta charset="UTF-8">
<title>风景 (Web view)</title>
<meta name="X-Original-Page-Id" content="{1AA1839C-D5BF-4AB7-99F3-793769C6259A}">
<style>
/* (For testing: Removed default CSS) */
@@ -1082,7 +1099,7 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: 风
<div class="title" style="left: 48px; position: absolute; top: 24px;"><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri Light; font-size: 20pt;">&nbsp;</span></div>
</div><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt;">Sunday, January 5, 2025</span></div>
<div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt;">10:13 PM</span></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><a href="onenote:#%E9%A3%8E%E6%99%AF&section-id={75256889-9e75-4ec2-82ed-fc799557e1b9}&page-id={d099b6f3-7f5a-4c08-aed7-e8d42c59523f}&end" style="font-family: Calibri; font-size: 11pt;">风景</a><span style="font-family: Calibri; font-size: 11pt;"> (</span><a href="https://onedrive.live.com/edit.aspx?resid=193EE54E3252492D!s9b62db4219f740709f444bc0129de4e9&migratedtospo=true&wd=target%28Quick%20Notes.one%7C75256889-9e75-4ec2-82ed-fc799557e1b9%2F%E9%A3%8E%E6%99%AF%7Cd099b6f3-7f5a-4c08-aed7-e8d42c59523f%2F%29&wdorigin=703&wdpreservelink=1" style="font-family: Calibri; font-size: 11pt;">Web view</a><span style="font-family: Calibri; font-size: 11pt;">)</span></p></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><a href=":/6" style="font-family: Calibri; font-size: 11pt;">风景</a><span style="font-family: Calibri; font-size: 11pt;"> (</span><a href="https://onedrive.live.com/edit.aspx?resid=193EE54E3252492D!s9b62db4219f740709f444bc0129de4e9&amp;migratedtospo=true&amp;wd=target%28Quick%20Notes.one%7C75256889-9e75-4ec2-82ed-fc799557e1b9%2F%E9%A3%8E%E6%99%AF%7Cd099b6f3-7f5a-4c08-aed7-e8d42c59523f%2F%29&amp;wdorigin=703&amp;wdpreservelink=1" style="font-family: Calibri; font-size: 11pt;">Web view</a><span style="font-family: Calibri; font-size: 11pt;">)</span></p></div>
</div>
<script>
@@ -1090,8 +1107,8 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: 风
window.parent.postMessage(window.location.href, '*');
}
</script>
</body>
</html>"
</body></html>"
`;
exports[`InteropService_Importer_OneNote should remove hyperlink from title: 风景 1`] = `
@@ -1100,6 +1117,7 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: 风
<head>
<meta charset="UTF-8">
<title>风景</title>
<meta name="X-Original-Page-Id" content="{D099B6F3-7F5A-4C08-AED7-E8D42C59523F}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -1130,6 +1148,7 @@ exports[`InteropService_Importer_OneNote should render audio as links to resourc
<head>
<meta charset="UTF-8">
<title>My title</title>
<meta name="X-Original-Page-Id" content="{301ACB0B-E552-428F-BA6E-0D5293ECCD35}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -1264,6 +1283,7 @@ exports[`InteropService_Importer_OneNote should render links properly by ignorin
<head>
<meta charset="UTF-8">
<title>Is Mexico safe for shooting Street Photography?</title>
<meta name="X-Original-Page-Id" content="{3DB79201-A9D3-4FBD-AAC5-2178D16CB20B}"/>
<style>
/* (For testing: Removed default CSS) */
@@ -1394,6 +1414,7 @@ exports[`InteropService_Importer_OneNote should support importing .one files tha
<html lang="en"><head>
<meta charset="UTF-8">
<title>Test Todo : cases à cocher lien vers doc sur partage</title>
<meta name="X-Original-Page-Id" content="{A51D95CD-67AF-43D6-9E5B-1A83068EA7AC}">
<style>
/* (For testing: Removed default CSS) */
@@ -1447,6 +1468,7 @@ exports[`InteropService_Importer_OneNote should use default value for EntityGuid
<html lang="en"><head>
<meta charset="UTF-8">
<title>Marketing Funnel &amp; Training</title>
<meta name="X-Original-Page-Id" content="{23857BB4-B769-4A90-931A-68122AAC3AF4}">
<style>
/* (For testing: Removed default CSS) */
@@ -1484,6 +1506,7 @@ exports[`InteropService_Importer_OneNote should use default value for EntityGuid
<head>
<meta charset="UTF-8">
<title>Decrease support costs</title>
<meta name="X-Original-Page-Id" content="{375A5D06-249C-574E-8439-23CEAA489035}"/>
<style>
/* (For testing: Removed default CSS) */

View File

@@ -103,7 +103,7 @@ export const checkIfLoginWasSuccessful = async (applicationsUrl: string) => {
const response = await fetch(applicationsUrl, {
headers: {
'X-JOPLIN-CUSTOM-API-KEY': '',
'X-JOPLIN-CUSTOM-API-KEY': Setting.value('sync.10.apiKey'),
},
});
const jsonBody = await response.json();

View File

@@ -86,6 +86,7 @@ export default class ShareService {
userContentBaseUrl: () => Setting.value(`sync.${syncTargetId}.userContentPath`),
username: () => Setting.value(`sync.${syncTargetId}.username`),
password: () => Setting.value(`sync.${syncTargetId}.password`),
apiKey: () => Setting.value(`sync.${syncTargetId}.apiKey`),
session: () => {
if (syncTargetId === 11) {
return {

View File

@@ -706,6 +706,7 @@ async function initFileApi() {
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
username: () => joplinServerAuth.email,
password: () => joplinServerAuth.password,
apiKey: () => '',
session: (): Session => null,
});

View File

@@ -13,7 +13,7 @@ const theme: Theme = {
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
oddBackgroundColor: '#141517',
color: '#dddddd',
colorError: 'red',
colorError: '#ff4444',
colorCorrect: '#72b972',
colorWarn: '#9A5B00',
colorWarnUrl: '#ffff82',

View File

@@ -4,6 +4,7 @@ use crate::onenote::outline::{Outline, parse_outline};
use crate::onenote::page_content::{PageContent, parse_page_content};
use crate::onestore::object_space::ObjectSpaceRef;
use crate::shared::exguid::ExGuid;
use crate::shared::guid::Guid;
use parser_utils::errors::{ErrorKind, Result};
use parser_utils::log::set_current_page;
@@ -15,6 +16,7 @@ use parser_utils::log::set_current_page;
/// [\[MS-ONE\] 2.2.19]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/e381b7c7-b434-43a2-ba23-0d08bafd281a
#[derive(Clone, Debug)]
pub struct Page {
entity_id: Guid,
title: Option<Title>,
level: i32,
author: Option<String>,
@@ -84,6 +86,12 @@ impl Page {
})
}
/// The page's GUID. May be referenced by internal links.
/// Ref: [ONESTORE 2.2.58](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-one/34ea5601-f060-4a69-b5f9-5843a1f14098)
pub fn link_target_id(&self) -> String {
format!("{}", self.entity_id)
}
fn outline_text(outline: &Outline) -> Option<&str> {
outline
.items
@@ -203,6 +211,7 @@ pub(crate) fn parse_page(page_space: ObjectSpaceRef) -> Result<Page> {
.collect::<Result<_>>()?;
Ok(Page {
entity_id: metadata.entity_guid,
title,
level,
author: data.author.map(|author| author.into_value()),

View File

@@ -1,4 +1,4 @@
use crate::page::Renderer;
use crate::{page::Renderer, utils::StyleSet};
use color_eyre::Result;
use parser::contents::EmbeddedFile;
use parser::property::embedded_file::FileType;
@@ -15,17 +15,25 @@ impl<'a> Renderer<'a> {
log!("Rendering embedded file: {:?}", path);
fs_driver().write_file(&path, file.data())?;
let file_type = Self::guess_type(file);
let mut styles = StyleSet::new();
if let Some(offset_x_half_inches) = file.offset_horizontal() {
styles.set("margin-left", format!("{}in", offset_x_half_inches / 2.));
}
if let Some(offset_y_half_inches) = file.offset_vertical() {
styles.set("margin-top", format!("{}in", offset_y_half_inches / 2.));
}
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
// 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),
FileType::Video => content = format!("<video controls src=\"{}\" {}></video>", filename, styles.to_html_attr()),
FileType::Unknown | FileType::Audio => {
content = format!(
"<p style=\"font-size: 11pt; line-height: 17px;\"><a href=\"{}\">{}</a></p>",
filename, filename
)
styles.set("font-size", "11pt".into());
styles.set("line-height", "17px".into());
let style_attr = styles.to_html_attr();
content = format!("<p {style_attr}><a href=\"{filename}\">{filename}</a></p>")
}
};

View File

@@ -71,7 +71,12 @@ impl<'a> Renderer<'a> {
content.push_str(&page_content);
crate::templates::page::render(&title_text, &content, &self.global_styles)
crate::templates::page::render(
&page.link_target_id(),
&title_text,
&content,
&self.global_styles
)
}
pub(crate) fn gen_class(&mut self, prefix: &str) -> String {

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<title>{{ name }}</title>
<meta name="X-Original-Page-Id" content="{{ page_id_attr }}"/>
<style>
/*** Start default CSS ***/
* { margin: 0; padding: 0; font-weight: normal; }

View File

@@ -1,4 +1,4 @@
use crate::utils::StyleSet;
use crate::utils::{StyleSet, html_entities};
use askama::Template;
use color_eyre::Result;
use color_eyre::eyre::WrapErr;
@@ -8,19 +8,22 @@ use std::collections::HashMap;
#[derive(Template)]
#[template(path = "page.html", escape = "none")]
struct PageTemplate<'a> {
page_id_attr: &'a str,
name: &'a str,
content: &'a str,
global_styles: Vec<(&'a String, &'a StyleSet)>,
}
pub(crate) fn render(
page_id: &str,
name: &str,
content: &str,
global_styles: &HashMap<String, StyleSet>,
) -> Result<String> {
PageTemplate {
name,
content,
name: &html_entities(name),
page_id_attr: &html_entities(page_id),
global_styles: global_styles
.iter()
.sorted_by(|(a, _), (b, _)| Ord::cmp(a, b))

View File

@@ -21,7 +21,7 @@
"@joplin/lib": "^3.5.1",
"@joplin/tools": "^3.5.1",
"@joplin/utils": "^3.5.1",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"gh-release-assets": "2.0.1",
"node-fetch": "2.6.7",
"source-map-support": "0.5.21",

File diff suppressed because one or more lines are too long

View File

@@ -25,7 +25,7 @@
"@types/node": "18.19.130",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"mermaid": "11.9.0",
"mermaid": "11.10.1",
"ts-jest": "29.4.1",
"typescript": "5.8.3"
},
@@ -36,7 +36,7 @@
"@types/json5": "2.2.0",
"abcjs": "6.5.2",
"font-awesome-filetypes": "2.1.0",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"highlight.js": "11.11.1",
"html-entities": "1.4.0",
"json-stringify-safe": "5.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "3.5.1",
"version": "3.5.2",
"private": true,
"scripts": {
"start-dev": "yarn build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -35,7 +35,7 @@
"compare-versions": "6.1.1",
"dayjs": "1.11.18",
"formidable": "2.1.2",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"html-entities": "1.4.0",
"jquery": "3.7.1",
"knex": "3.1.0",

Some files were not shown because too many files have changed in this diff Show More