1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-04-18 19:42:23 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Laurent Cozic 04389e6c87 Keep the index.js validation on mobile. 2026-04-14 16:13:57 +01:00
Laurent Cozic 20b5e02802 Fixed loading 2026-04-14 12:55:06 +01:00
Laurent Cozic 204b653422 update 2026-04-14 11:02:54 +01:00
36 changed files with 142 additions and 372 deletions
+2
View File
@@ -360,6 +360,8 @@ packages/app-desktop/gui/NoteList/utils/useMoveNote.js
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
packages/app-desktop/gui/NoteList/utils/useOnNoteDoubleClick.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.test.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.js
packages/app-desktop/gui/NoteList/utils/useScroll.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
+2
View File
@@ -333,6 +333,8 @@ packages/app-desktop/gui/NoteList/utils/useMoveNote.js
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
packages/app-desktop/gui/NoteList/utils/useOnNoteDoubleClick.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.test.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.js
packages/app-desktop/gui/NoteList/utils/useScroll.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
-2
View File
@@ -12,8 +12,6 @@
- Avoid duplicating code in tests; when testing the same logic with different inputs, use `test.each` or shared helpers instead of repeating similar test blocks.
- Do not make white space changes - do not add unnecessary new lines, or spaces to existing code, or wrap existing code.
- If you add a new TypeScript file, run `yarn updateIgnored` from the root.
- When an unknown word is detected by cSpell, handle is as per the specification in `readme/dev/spellcheck.md`
- To compile TypeScript, use `yarn tsc`. To type-check without emitting files, use `yarn tsc --noEmit`.
## Full Documentation
@@ -31,6 +31,7 @@ import { stateUtils } from '@joplin/lib/reducer';
import { connect } from 'react-redux';
import useOnNoteDoubleClick from './utils/useOnNoteDoubleClick';
import useAutoScroll from './utils/useAutoScroll';
import useRefocusOnDeletion from './utils/useRefocusOnDeletion';
const commands = {
focusElementNoteList,
@@ -74,6 +75,7 @@ const NoteList = (props: Props) => {
const { activeNoteId, setActiveNoteId } = useActiveDescendantId(props.selectedFolderId, props.selectedNoteIds);
const focusNote = useFocusNote(listRef, props.notes, makeItemIndexVisible, setActiveNoteId);
useRefocusOnDeletion(props.notes.length, props.selectedNoteIds, props.focusedField, props.selectedFolderId, focusNote);
const moveNote = useMoveNote(
props.notesParentType,
@@ -0,0 +1,41 @@
import { renderHook } from '@testing-library/react';
import useRefocusOnDeletion from './useRefocusOnDeletion';
describe('useRefocusOnDeletion', () => {
it('should refocus when a note is deleted in the same folder', () => {
const focusNote = jest.fn();
const { rerender } = renderHook(
({ noteCount }: { noteCount: number }) =>
useRefocusOnDeletion(noteCount, ['note-1'], '', 'folder-1', focusNote),
{ initialProps: { noteCount: 3 } },
);
rerender({ noteCount: 2 });
expect(focusNote).toHaveBeenCalledWith('note-1');
});
test.each([
['note count increases', 2, 3, '', ['note-1']],
['another field has focus', 3, 2, 'editor', ['note-1']],
['multiple notes are selected', 3, 2, '', ['note-1', 'note-2']],
])('should not refocus when %s', (_label, initialCount, newCount, focusedField, noteIds) => {
const focusNote = jest.fn();
const { rerender } = renderHook(
({ noteCount }: { noteCount: number }) =>
useRefocusOnDeletion(noteCount, noteIds, focusedField, 'folder-1', focusNote),
{ initialProps: { noteCount: initialCount } },
);
rerender({ noteCount: newCount });
expect(focusNote).not.toHaveBeenCalled();
});
it('should not refocus when switching to a folder with fewer notes', () => {
const focusNote = jest.fn();
const { rerender } = renderHook(
({ noteCount, folderId }: { noteCount: number; folderId: string }) =>
useRefocusOnDeletion(noteCount, ['note-1'], '', folderId, focusNote),
{ initialProps: { noteCount: 3, folderId: 'folder-1' } },
);
rerender({ noteCount: 2, folderId: 'folder-2' });
expect(focusNote).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,20 @@
import { useEffect } from 'react';
import usePrevious from '@joplin/lib/hooks/usePrevious';
const useRefocusOnDeletion = (
noteCount: number,
selectedNoteIds: string[],
focusedField: string,
selectedFolderId: string,
focusNote: (noteId: string)=> void,
) => {
const previousNoteCount = usePrevious(noteCount, 0);
const previousFolderId = usePrevious(selectedFolderId, '');
useEffect(() => {
const noteWasRemoved = noteCount < previousNoteCount;
const folderDidNotChange = selectedFolderId === previousFolderId;
if (noteWasRemoved && folderDidNotChange && selectedNoteIds.length === 1 && !focusedField) {
focusNote(selectedNoteIds[0]);
}
}, [noteCount, previousNoteCount, selectedNoteIds, focusedField, selectedFolderId, previousFolderId, focusNote]);
};
export default useRefocusOnDeletion;
@@ -7,40 +7,26 @@ import activateMainMenuItem from './util/activateMainMenuItem';
import setSettingValue from './util/setSettingValue';
import { toForwardSlashes } from '@joplin/utils/path';
import mockClipboard from './util/mockClipboard';
import { ElectronApplication, Page } from '@playwright/test';
const importAndOpenHtmlExport = async (mainWindow: Page, electronApp: ElectronApplication, noteTitle: string) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.waitFor();
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
await importedFolder.waitFor();
// Retry -- focusing the imported-folder may fail in some cases
await expect(async () => {
await importedFolder.click();
await mainScreen.noteList.focusContent(electronApp);
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle(noteTitle);
await importedHtmlFileItem.click({ timeout: 300 });
}).toPass();
return { mainScreen };
};
test.describe('markdownEditor', () => {
test('editor should render the full content of HTML notes', async ({ mainWindow, electronApp }) => {
const { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-spans');
const editor = mainScreen.noteEditor.codeMirrorEditor;
// Regression test: The <span> should not be hidden by inline Markdown rendering (since this is an HTML note):
await expect(editor).toHaveText('<p><span style="margin-left: 100px;">test</span></p>');
});
test('preview pane should render images in HTML notes', async ({ mainWindow, electronApp }) => {
const { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-image');
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.waitFor();
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
await importedFolder.waitFor();
// Retry -- focusing the imported-folder may fail in some cases
await expect(async () => {
await importedFolder.click();
await mainScreen.noteList.focusContent(electronApp);
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
await importedHtmlFileItem.click({ timeout: 300 });
}).toPass();
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
// Should render headers
@@ -101,35 +101,6 @@ test.describe('noteList', () => {
await expect(testNoteItem).toBeVisible();
});
test('should remain focused after deleting a note to the trash', async ({ electronApp, mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('test note 1');
await mainScreen.createNewNote('test note 2');
await mainScreen.createNewNote('test note 3');
const noteList = mainScreen.noteList;
await noteList.sortByTitle(electronApp);
await noteList.focusContent(electronApp);
// The most-recently created note should be selected
await noteList.expectNoteToBeSelected('test note 3');
// All three notes should be visible
const getNote = (i: number) => noteList.getNoteItemByTitle(`test note ${i}`);
await expect(getNote(1)).toBeVisible();
await expect(getNote(2)).toBeVisible();
await expect(getNote(3)).toBeVisible();
await getNote(3).press('Delete');
await expect(getNote(3)).not.toBeVisible();
// Pressing the up arrow should change the selection
// (Regression test for https://github.com/laurent22/joplin/issues/10753)
await noteList.expectNoteToBeSelected('test note 2');
await noteList.container.press('ArrowUp');
await noteList.expectNoteToBeSelected('test note 1');
});
test('arrow keys should navigate the note list', async ({ electronApp, mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
const sidebar = mainScreen.sidebar;
@@ -1 +0,0 @@
<p><span style="margin-left: 100px;">test</span></p>
@@ -1,6 +1,5 @@
import { test, expect } from './util/test';
import MainScreen from './models/MainScreen';
import { Second } from '@joplin/utils/time';
test.describe('sidebar', () => {
test('should be able to create new folders', async ({ mainWindow }) => {
@@ -45,54 +44,6 @@ test.describe('sidebar', () => {
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
});
// Regression test for https://github.com/laurent22/joplin/issues/15029
test('should remain focused when navigating with the arrow keys', async ({ electronApp, mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
const sidebar = mainScreen.sidebar;
// Build the folder hierarchy: Navigating upwards through the list
// should transition from a notebook with more notes to a notebook with
// fewer notes.
const folderAHeader = await sidebar.createNewFolder('Folder A');
await mainScreen.createNewNote('Test');
await expect(folderAHeader).toBeVisible();
const folderBHeader = await sidebar.createNewFolder('Folder B');
await mainScreen.createNewNote('Test 2');
await mainScreen.createNewNote('Test 3');
const folderCHeader = await sidebar.createNewFolder('Folder C');
const folderDHeader = await sidebar.createNewFolder('Folder D');
await folderBHeader.dragTo(folderAHeader);
await folderCHeader.dragTo(folderAHeader);
// Should have the correct initial state
await sidebar.forceUpdateSorting(electronApp);
await sidebar.expectToHaveDepths([
[folderAHeader, 2],
[folderBHeader, 3],
[folderCHeader, 3],
[folderDHeader, 2],
]);
const assertFocused = async (title: RegExp) => {
await expect(mainWindow.locator(':focus')).toHaveText(title);
// Pause to help check that focus is stable. This is present to help this test more reliably detect
// timing-related issues.
await mainWindow.waitForTimeout(Second);
await expect(mainWindow.locator(':focus')).toHaveText(title);
};
await folderDHeader.click();
// Focus should remain on the correct folder header while navigating
await mainWindow.keyboard.press('ArrowUp');
await assertFocused(/^Folder C/);
await mainWindow.keyboard.press('ArrowUp');
await assertFocused(/^Folder B/);
await mainWindow.keyboard.press('ArrowUp');
await assertFocused(/^Folder A/);
});
test('should allow changing the focused folder by pressing the first character of the title', async ({ electronApp, mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
const sidebar = mainScreen.sidebar;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.6.9",
"version": "3.6.8",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,
+2 -2
View File
@@ -83,8 +83,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097804
versionName "3.6.16"
versionCode 2097803
versionName "3.6.15"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
@@ -46,12 +46,18 @@ export default class PluginRunner extends BasePluginRunner {
return false;
});
// On native mobile, pass a file path so the WebView can load the
// script directly from the filesystem (avoids transferring the full
// script text across the React Native bridge). On web, file:// URLs
// are blocked by CSP so we pass the script text directly.
const scriptFilePath = plugin.scriptText ? '' : `${plugin.baseDir}/index.js`;
this.webviewRef.current.injectJS(`
pluginBackgroundPage.runPlugin(
${JSON.stringify(shim.injectedJs('pluginBackgroundPage'))},
${JSON.stringify(plugin.scriptText)},
${JSON.stringify(scriptFilePath)},
${JSON.stringify(messageChannelId)},
${JSON.stringify(plugin.id)},
${JSON.stringify(plugin.scriptText)},
);
`);
@@ -186,6 +186,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
html={html}
injectedJavaScript={injectedJs}
hasPluginScripts={true}
allowFileAccessFromJs={true}
onMessage={pluginRunner.onWebviewMessage}
onLoadEnd={onLoadEnd}
onLoadStart={onLoadStart}
@@ -26,14 +26,29 @@ export const stopPlugin = async (pluginId: string) => {
delete loadedPlugins[pluginId];
};
export const runPlugin = (
pluginBackgroundScript: string, pluginScript: string, messageChannelId: string, pluginId: string,
export const runPlugin = async (
pluginBackgroundScript: string, scriptFilePath: string, messageChannelId: string, pluginId: string, scriptText = '',
) => {
if (loadedPlugins[pluginId]) {
console.warn(`Plugin already running ${pluginId}`);
return;
}
// When scriptText is provided (web), use it directly. Otherwise load
// the plugin script from the filesystem (native mobile). We use
// XMLHttpRequest because fetch() doesn't support file:// URLs on
// Android WebView.
let pluginScript = scriptText;
if (!pluginScript) {
pluginScript = await new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `file://${scriptFilePath}`, true);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(new Error(`Failed to load plugin script: ${scriptFilePath}`));
xhr.send();
});
}
const bodyHtml = '';
const initialJavaScript = `
"use strict";
+4 -4
View File
@@ -2066,7 +2066,7 @@ PODS:
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (12.2.2):
- RNShare (12.2.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2133,7 +2133,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- SDWebImage/Core (5.21.5)
- SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
@@ -2632,9 +2632,9 @@ SPEC CHECKSUMS:
RNLocalize: 44b09911588826d01c5b949e8e3f9ed5fae16b32
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: a075abc351f03fd89517bbee912593f299eb8a64
RNShare: 0e600372fb35783fe30d413efd28d11de2bf6cf0
RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
WhisperVoiceTyping: 343ea840cbde2a5f3508f8b016ebcf1c089179ea
Yoga: 786fa7d9d2ff6060b4e688062243fa69c323d140
-1
View File
@@ -7,7 +7,6 @@
"scripts": {
"start": "BROWSERSLIST_IGNORE_OLD_DATA=true react-native start --reset-cache",
"android": "react-native run-android",
"android-log": "adb logcat 'ReactNative:V' 'ReactNativeJS:V' 'chromium:V' '*:S'",
"build": "NO_FLIPPER=1 gulp build",
"web": "webpack --mode production --config ./web/webpack.config.ts --progress && cp -r ./web/public/* ./web/dist/",
"serve-web-hot-reload": "yarn serve-web --env HOT_RELOAD",
@@ -109,9 +109,7 @@ const configFromSettings = (settings: EditorSettings, context: RenderedContentCo
extensions.push(Prec.low(keymap.of(defaultKeymap)));
}
// Only enable in-editor rendering for Markdown notes. In-editor rendering can result in
// confusing output in HTML notes (e.g. some, but not most, tags hidden).
if (settings.inlineRenderingEnabled && settings.language === EditorLanguageType.Markdown) {
if (settings.inlineRenderingEnabled) {
extensions.push(renderingExtension());
}
@@ -350,8 +350,16 @@ export default class PluginService extends BaseService {
logger.info(`Loading plugin from ${path}`);
const scriptText = await fsDriver.readFile(`${distPath}/index.js`);
const manifestText = await fsDriver.readFile(`${distPath}/manifest.json`);
// On mobile, plugin scripts are loaded directly by the WebView
// from the filesystem, so we don't need to read them here.
const indexPath = `${distPath}/index.js`;
if (shim.mobilePlatform()) {
if (!(await fsDriver.exists(indexPath))) {
throw new Error(`Plugin bundle not found at: ${indexPath}`);
}
}
const scriptText = shim.mobilePlatform() ? '' : await fsDriver.readFile(indexPath);
const pluginId = makePluginId(filename(path));
return this.loadPlugin(distPath, manifestText, scriptText, pluginId);
-16
View File
@@ -90,23 +90,7 @@ Suppose that the importer's Rust code is failing to parse a specific `example.on
2. Setting up Rust and Rust debugging. See [the relevant VSCode documentation](https://code.visualstudio.com/docs/languages/rust#_debugging) for details.
3. Clicking the "Debug" button for the test added in step 1. This button should be provided by extensions set up in step 2.
### Inspecting `.one` files
The `inspect` binary target of the `parser` crate allows inspecting `.one` file data.
For example, to inspect lower-level OneStore data:
```console
bash$ cd parser/
bash$ cargo run -- ../test-data/ink.one --onestore
```
To inspect higher-level (parsed) section data:
```console
bash$ cd parser/
bash$ cargo run -- ../test-data/ink.one --section
```
**Note**: `inspect`'s output is unstable and should not be relied upon by scripts.
### Developing
@@ -1,15 +0,0 @@
/// A struct that has a specific `fmt::Debug` serialization.
/// Useful when customizing a `struct`'s debug output.
pub struct DebugOutput<'a>(&'a str);
impl<'a> From<&'a str> for DebugOutput<'a> {
fn from(value: &'a str) -> Self {
Self(value)
}
}
impl<'a> std::fmt::Debug for DebugOutput<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
@@ -43,12 +43,6 @@ impl From<widestring::error::MissingNulTerminator> for Error {
}
}
impl From<widestring::error::Utf16Error> for Error {
fn from(err: widestring::error::Utf16Error) -> Self {
ErrorKind::from(err).into()
}
}
impl From<uuid::Error> for Error {
fn from(err: uuid::Error) -> Self {
ErrorKind::from(err).into()
@@ -134,13 +128,6 @@ pub enum ErrorKind {
err: string::FromUtf16Error,
},
/// A different type of malformed UTF-16 string was encountered during parsing.
#[error("Malformed UTF-16 string: {err}")]
Utf16LibError {
#[from]
err: widestring::error::Utf16Error,
},
/// A UTF-16 string without a null terminator was encountered during parsing.
#[error("UTF-16 string is missing null terminator: {err}")]
Utf16MissingNull {
@@ -2,7 +2,6 @@
use widestring::U16CString;
pub mod debug;
pub mod errors;
mod file_api;
pub mod log;
@@ -27,6 +26,6 @@ impl Utf16ToString for &[u8] {
.collect();
let value = U16CString::from_vec_truncate(data);
value.to_string().map_err(|err| err.into())
Ok(value.to_string().unwrap())
}
}
@@ -33,6 +33,3 @@ features = [
[lib]
crate-type = ["cdylib", "lib"]
[[bin]]
name = "inspect"
@@ -1,121 +0,0 @@
use parser::Parser;
use parser_utils::errors::Error;
use std::{
env::{self, Args},
path::PathBuf,
process::exit,
};
pub fn main() {
let config = match Config::from_args(&mut env::args()) {
Ok(config) => config,
Err(error) => {
print_help_text(&error.program_name, error.reason);
exit(1)
}
};
let input_path_string = &config.input_file.to_string_lossy();
eprintln!("Reading {}", input_path_string);
let data = match std::fs::read(&config.input_file) {
Ok(data) => data,
Err(error) => {
let error = format!("File read error: {error}");
print_help_text(&config.program_name, &error);
exit(2)
}
};
let mut parser = Parser::new();
if config.output_mode == OutputMode::Section {
let parsed_section = match parser.parse_section_from_data(&data, input_path_string) {
Ok(section) => section,
Err(error) => handle_parse_error(&config, error),
};
println!("{:#?}", parsed_section);
} else {
let parsed_onestore = match parser.parse_onestore_raw(&data) {
Ok(section) => section,
Err(error) => handle_parse_error(&config, error),
};
println!("{:#?}", parsed_onestore);
}
}
fn handle_parse_error(config: &Config, error: Error) -> ! {
let error = format!("Parse error: {error}");
print_help_text(&config.program_name, &error);
exit(3)
}
fn print_help_text(program_name: &str, error: &str) {
let error_info = if error.is_empty() { "" } else { error };
eprintln!("Usage: {program_name} <input_file> [--section|--onestore]");
eprintln!("Description: Prints debug information about the given <input_file>");
eprintln!("{error_info}");
}
struct ConfigParseError {
reason: &'static str,
program_name: String,
}
#[derive(PartialEq)]
enum OutputMode {
/// Lower-level output
FileContent,
/// Higher-level output, including the parsed objects
Section,
}
struct Config {
input_file: PathBuf,
output_mode: OutputMode,
program_name: String,
}
impl Config {
pub fn from_args(args: &mut Args) -> Result<Self, ConfigParseError> {
let Some(program_name) = &args.next() else {
return Err(ConfigParseError {
reason: "Missing program name",
program_name: "??".into(),
});
};
let program_name = program_name.to_string();
let Some(input_file) = &args.next() else {
return Err(ConfigParseError {
reason: "Not enough arguments",
program_name,
});
};
let output_mode = args.next().unwrap_or("--onestore".into());
let output_mode = match output_mode.as_str() {
"--onestore" => Ok(OutputMode::FileContent),
"--section" => Ok(OutputMode::Section),
_ => {
return Err(ConfigParseError {
reason: "Invalid output mode (expected --onestore or --section)",
program_name,
});
}
}?;
if args.next().is_some() {
return Err(ConfigParseError {
reason: "Too many arguments",
program_name,
});
}
Ok(Config {
input_file: input_file.into(),
output_mode,
program_name,
})
}
}
@@ -539,7 +539,7 @@ pub struct RootObjectReference3FND {
pub struct RevisionRoleDeclarationFND {
pub rid: ExGuid,
/// "should be 0x01"
pub revision_role: u32,
revision_role: u32,
}
#[derive(Debug, Clone, Parse)]
@@ -6,7 +6,7 @@ use crate::{
},
shared::exguid::ExGuid,
};
use parser_utils::errors::Result;
use parser_utils::{errors::Result, log};
use std::fmt::Debug;
use std::rc::Rc;
@@ -73,9 +73,8 @@ impl ObjectGroupList {
if matches!(item, FileNodeData::ObjectGroupEndFND) {
break;
} else if let FileNodeData::DataSignatureGroupDefinitionFND(_) = item {
// Marks the end of a signature block. Ignored.
// See https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/0fa4c886-011a-4c19-9651-9a69e43a19c6
iterator.next();
log!("Ignoring DataSignatureGroupDefinitionFND");
} else if let Some(object) = Object::try_parse(iterator, &parse_context)? {
objects.push(Rc::new(object));
} else {
@@ -60,15 +60,8 @@ impl RevisionManifestList {
let revision = revisions_map.get(&data.base.rid);
if let Some(_revision) = revision {
iterator.next();
// According to MS-ONESTORE 2.1.12, revision_role *should* always be 0x1
if data.base.revision_role != 0x1 {
// TODO: Find a test .one file that uses this and implement:
log_warn!(
"TO-DO: Apply the new role and context to the revision (role {:x})",
data.base.revision_role
);
}
// TODO: Find a test .one file that uses this and implement:
log_warn!("TO-DO: Apply the new role and context to the revision");
} else {
return Err(
ErrorKind::MalformedOneStoreData("RevisionRoleAndContextDeclarationFND points to a non-existent revision".into()).into()
@@ -1,8 +1,6 @@
use std::rc::Rc;
use crate::onenote::notebook::Notebook;
use crate::onenote::section::{Section, SectionEntry, SectionGroup};
use crate::onestore::{OneStore, OneStoreType, parse_onestore};
use crate::onestore::{OneStoreType, parse_onestore};
use parser_utils::errors::{ErrorKind, Result};
use parser_utils::{fs_driver, log, reader::Reader};
@@ -75,11 +73,6 @@ impl Parser {
self.parse_section_from_data(&data, &path)
}
/// Parses low-level OneStore data
pub fn parse_onestore_raw(&mut self, data: &[u8]) -> Result<Rc<dyn OneStore>> {
parse_onestore(&mut Reader::new(data))
}
/// Parse a OneNote section file from a byte array.
/// The [path] is used to provide debugging information and determine
/// the name of the section file.
@@ -21,7 +21,7 @@ pub mod mapping_table;
pub mod object;
pub mod object_space;
pub trait OneStore: std::fmt::Debug {
pub trait OneStore {
fn get_type(&self) -> OneStoreType;
fn data_root(&self) -> ObjectSpaceRef;
/// Fetches the object space that is parent to the object identified by the
@@ -22,7 +22,7 @@ impl ObjectFileData for FileBlob {
/// See [\[MS-ONESTORE\] 2.1.5](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/ce60b62f-82e5-401a-bf2c-3255457732ad)
#[derive(Clone)]
pub struct Object {
pub(crate) struct Object {
pub(crate) context_id: ExGuid,
pub(crate) jc_id: JcId,
@@ -51,11 +51,11 @@ impl std::fmt::Debug for Object {
}
impl Object {
pub(crate) fn id(&self) -> JcId {
pub fn id(&self) -> JcId {
self.jc_id
}
pub(crate) fn props(&self) -> &ObjectPropSet {
pub fn props(&self) -> &ObjectPropSet {
&self.props
}
@@ -1,8 +1,6 @@
use crate::one::property::PropertyType;
use crate::shared::property::{PropertyId, PropertyValue};
use parser_utils::Reader;
use parser_utils::Utf16ToString;
use parser_utils::debug::DebugOutput;
use parser_utils::errors::Result;
use std::collections::HashMap;
use std::fmt::Debug;
@@ -27,41 +25,12 @@ pub(crate) struct PropertySet {
impl Debug for PropertySet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn format_value(value: &PropertyValue) -> String {
match value {
PropertyValue::Vec(vec) => {
// Vec() property values are used to represent strings. Try creating a string representation for
// debugging purposes:
let s = vec
.as_slice()
// OneNote file strings are usually UTF-16
.utf16_to_string()
.unwrap_or("".to_string());
// Heuristic: If the text contains at least one ASCII letter/space character, it's probably a string.
// This will miss some non-ASCII strings and incorrectly print some non-string vecs.
let is_probably_string = !s.is_empty()
&& s.chars()
.any(|c| c.is_ascii_whitespace() || c.is_ascii_alphanumeric());
if is_probably_string {
format!("{:?} ({:?})", s, vec)
} else {
format!("{:?}", vec)
}
}
// Use the default compact representation of the value.
// This keeps potentially-long property values on a single line when producing
// multi-line debug output, which is usually more readable.
_ => format!("{:?}", value),
}
}
let mut debug_map = f.debug_map();
for (key, (_, value)) in &self.values {
let formatted_key = format!("{:#0x}", key);
let formatted_value = format_value(value);
debug_map.entry(&formatted_key, &DebugOutput::from(formatted_value.as_str()));
// Use the default compact representation of the value
let formatted_value = format!("{:?}", value);
debug_map.entry(&formatted_key, &formatted_value);
}
debug_map.finish()
}
-2
View File
@@ -261,5 +261,3 @@ llamacpp
bgcolor
bordercolor
togglefullscreen
onestore
pdate
-2
View File
@@ -38,8 +38,6 @@ describe('git-changelog', () => {
['Update aws-sdk-js-v3 monorepo to v3.215.0', 'aws-sdk-js-v3', 'v3.215.0'],
['Update dependency moment to v2.29.4 (#7087)', 'moment', 'v2.29.4'],
['Update aws (#8106)', 'aws', ''],
['fix(deps): update dependency prosemirror-gapcursor to v1.4.0 (#15069)', 'prosemirror-gapcursor', 'v1.4.0'],
['chore(deps): update dependency webpack-dev-server to v5.2.3 (#15078)', 'webpack-dev-server', 'v5.2.3'],
];
for (const testCase of testCases) {
+3 -3
View File
@@ -145,9 +145,9 @@ export interface RenovateMessage {
export const parseRenovateMessage = (message: string): RenovateMessage => {
const regexes = [
/^(?:(?:fix|chore)\(deps\): )?[Uu]pdate dependency ([^\s]+) to ([^\s]+)/,
/^(?:(?:fix|chore)\(deps\): )?[Uu]pdate ([^\s]+) monorepo to ([^\s]+)/,
/^(?:(?:fix|chore)\(deps\): )?[Uu]pdate ([^\s]+)/,
/^Update dependency ([^\s]+) to ([^\s]+)/,
/^Update ([^\s]+) monorepo to ([^\s]+)/,
/^Update ([^\s]+)/,
];
for (const regex of regexes) {
-10
View File
@@ -1,15 +1,5 @@
# Joplin Android Changelog
## [android-v3.6.16](https://github.com/laurent22/joplin/releases/tag/android-v3.6.16) - 2026-04-14T15:38:56Z
- New: Add 'Go to start/end of note' toolbar buttons (#15015 by [@Vpatel1093](https://github.com/Vpatel1093))
- Improved: Updated packages react-native-share (v12.2.2), sass (v1.95.1)
- Fixed: Fix Android markdown editor text replacement (characters disappearing during typing) (#15007) (#13134 by Sriram Varun Kumar)
- Fixed: Fix back button disabled after navigating away from a deleted notebook (#15028) (#15004 by Sriram Varun Kumar)
- Fixed: Fix profile list not scrollable to last item on Manage Profiles screen (#15074) (#15061 by Sriram Varun Kumar)
- Fixed: Fix shared note not persisted to active notebook (#15064) (#15060 by Sriram Varun Kumar)
- Fixed: Migrate expo-av to expo-audio (#14847) (#14804 by [@gherardi](https://github.com/gherardi))
## [android-v3.6.15](https://github.com/laurent22/joplin/releases/tag/android-v3.6.15) - 2026-04-05T13:00:51Z
- New: Add toolbar button reordering with up/down arrows (#14485 by [@Vpatel1093](https://github.com/Vpatel1093))