You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-23 23:33:01 +02:00
Compare commits
47 Commits
android-v3
...
android-v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5598242f9 | ||
|
|
57980ae916 | ||
|
|
9d1720b6e1 | ||
|
|
c4e0ed18eb | ||
|
|
150f6c9a3f | ||
|
|
6f3781f27a | ||
|
|
37c3d24650 | ||
|
|
bcb3f69d15 | ||
|
|
70ffb29af4 | ||
|
|
5f61bee712 | ||
|
|
496d007f74 | ||
|
|
5a9b389504 | ||
|
|
107290177e | ||
|
|
5055c9af3e | ||
|
|
2ed6650136 | ||
|
|
e80db6afb5 | ||
|
|
6a06922633 | ||
|
|
fd02d88739 | ||
|
|
dacd460f64 | ||
|
|
3279485f44 | ||
|
|
eaf8d15be7 | ||
|
|
6b186b965a | ||
|
|
7a8ac14c99 | ||
|
|
73291fa355 | ||
|
|
27ff8be432 | ||
|
|
0904838311 | ||
|
|
2798cc6027 | ||
|
|
1ede5bc499 | ||
|
|
418a660a66 | ||
|
|
5bc073e888 | ||
|
|
87b443e051 | ||
|
|
8e36644068 | ||
|
|
1833de789a | ||
|
|
0b18fd988b | ||
|
|
2ce65b9315 | ||
|
|
8f4f0ee321 | ||
|
|
6a83cc95ee | ||
|
|
5134b63075 | ||
|
|
74527d7006 | ||
|
|
ad909ac6f0 | ||
|
|
5ff0285b85 | ||
|
|
bcb509a965 | ||
|
|
075c98175e | ||
|
|
212112d4b6 | ||
|
|
74bf0cb655 | ||
|
|
b2bdf84f06 | ||
|
|
a2156a0548 |
@@ -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
3
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -107,6 +107,7 @@ class Command extends BaseCommand {
|
||||
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
|
||||
username: () => joplinServerAuth.email,
|
||||
password: () => joplinServerAuth.password,
|
||||
apiKey: () => '',
|
||||
session: (): Session => null,
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
74
packages/app-cli/tests/html_to_md/wikipedia_math_inline.html
Normal file
74
packages/app-cli/tests/html_to_md/wikipedia_math_inline.html
Normal 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&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\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 </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 </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&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\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;"/>
|
||||
@@ -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}}}$
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}/>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,12 +28,14 @@ const defaultEditorProps = {
|
||||
globalSearch: '',
|
||||
noteId: '',
|
||||
noteHash: '',
|
||||
initialScroll: 0,
|
||||
style: {},
|
||||
toolbarEnabled: true,
|
||||
readOnly: false,
|
||||
onChange: ()=>{},
|
||||
onSelectionChange: ()=>{},
|
||||
onUndoRedoDepthChange: ()=>{},
|
||||
onScroll: ()=>{},
|
||||
onAttach: async ()=>{},
|
||||
noteResources: {},
|
||||
plugins: {},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} `));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -12,6 +12,7 @@ const defaultWrapperProps: EditorProps = {
|
||||
noteHash: '',
|
||||
noteId: '',
|
||||
initialText: '',
|
||||
initialScroll: 0,
|
||||
editorSettings: defaultEditorSettings,
|
||||
initialSelection: { start: 0, end: 0 },
|
||||
globalSearch: '',
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface EditorProps {
|
||||
noteHash: string;
|
||||
initialText: string;
|
||||
initialSelection: SelectionRange;
|
||||
initialScroll: number;
|
||||
editorSettings: EditorSettings;
|
||||
globalSearch: string;
|
||||
plugins: PluginStates;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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/.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/utils": "~3.5",
|
||||
"fs-extra": "11.3.1",
|
||||
"fs-extra": "11.3.2",
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
22
packages/editor/ProseMirror/commands/selectDocumentEnd.ts
Normal file
22
packages/editor/ProseMirror/commands/selectDocumentEnd.ts
Normal 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;
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -161,6 +161,7 @@ const nodes = addDefaultToplevelAttributes({
|
||||
inline: true,
|
||||
group: 'inlineBreak',
|
||||
selectable: false,
|
||||
leafText: () => '\n',
|
||||
parseDOM: [{ tag: 'br' }],
|
||||
toDOM: () => domOutputSpecs.br,
|
||||
},
|
||||
|
||||
19
packages/editor/ProseMirror/utils/clampPointToDocument.ts
Normal file
19
packages/editor/ProseMirror/utils/clampPointToDocument.ts
Normal 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;
|
||||
8
packages/editor/ProseMirror/utils/getTextBetween.ts
Normal file
8
packages/editor/ProseMirror/utils/getTextBetween.ts
Normal 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;
|
||||
@@ -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>
|
||||
`),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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. §ion-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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;"> </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;"> </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§ion-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&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>
|
||||
|
||||
<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 & 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) */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -706,6 +706,7 @@ async function initFileApi() {
|
||||
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
|
||||
username: () => joplinServerAuth.email,
|
||||
password: () => joplinServerAuth.password,
|
||||
apiKey: () => '',
|
||||
session: (): Session => null,
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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>")
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
546
packages/renderer/assets/mermaid/mermaid.min.js
vendored
546
packages/renderer/assets/mermaid/mermaid.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user