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

Compare commits

...

6 Commits

Author SHA1 Message Date
Laurent Cozic be1a018746 Desktop release v3.6.9 2026-04-14 13:00:42 +01:00
Laurent Cozic ae390469b5 Lock file 2026-04-14 12:57:10 +01:00
Laurent Cozic c31a7392cc Chore: Add android-log command to package.json 2026-04-14 12:56:50 +01:00
Henry Heino 9ca213cefd Desktop: Fixes #15029: Accessibility: Fix focus unexpectedly jumps to the note list while editing/navigating (#15090) 2026-04-14 11:12:12 +01:00
Henry Heino a6a5ab9bc9 Desktop: Markdown editor: Fix unselected <span>s are hidden in HTML notes (#15089) 2026-04-14 11:12:01 +01:00
Henry Heino 0a94d02795 Chore: Importing from OneNote: Add debug tool for inspecting .one files (#15084) 2026-04-14 11:07:46 +01:00
27 changed files with 348 additions and 103 deletions
-2
View File
@@ -360,8 +360,6 @@ 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,8 +333,6 @@ 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
@@ -31,7 +31,6 @@ 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,
@@ -75,7 +74,6 @@ 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,
@@ -1,41 +0,0 @@
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();
});
});
@@ -1,20 +0,0 @@
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,26 +7,40 @@ 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 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 { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-image');
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
// Should render headers
@@ -101,6 +101,35 @@ 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;
@@ -0,0 +1 @@
<p><span style="margin-left: 100px;">test</span></p>
@@ -1,5 +1,6 @@
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 }) => {
@@ -44,6 +45,54 @@ 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.8",
"version": "3.6.9",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,
+4 -4
View File
@@ -2066,7 +2066,7 @@ PODS:
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (12.2.1):
- RNShare (12.2.2):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2133,7 +2133,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- SDWebImage/Core (5.21.7)
- SDWebImage/Core (5.21.5)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
@@ -2632,9 +2632,9 @@ SPEC CHECKSUMS:
RNLocalize: 44b09911588826d01c5b949e8e3f9ed5fae16b32
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 0e600372fb35783fe30d413efd28d11de2bf6cf0
RNShare: a075abc351f03fd89517bbee912593f299eb8a64
RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
WhisperVoiceTyping: 343ea840cbde2a5f3508f8b016ebcf1c089179ea
Yoga: 786fa7d9d2ff6060b4e688062243fa69c323d140
+1
View File
@@ -7,6 +7,7 @@
"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,7 +109,9 @@ const configFromSettings = (settings: EditorSettings, context: RenderedContentCo
extensions.push(Prec.low(keymap.of(defaultKeymap)));
}
if (settings.inlineRenderingEnabled) {
// 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) {
extensions.push(renderingExtension());
}
+16
View File
@@ -90,7 +90,23 @@ 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
@@ -0,0 +1,15 @@
/// 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,6 +43,12 @@ 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()
@@ -128,6 +134,13 @@ 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,6 +2,7 @@
use widestring::U16CString;
pub mod debug;
pub mod errors;
mod file_api;
pub mod log;
@@ -26,6 +27,6 @@ impl Utf16ToString for &[u8] {
.collect();
let value = U16CString::from_vec_truncate(data);
Ok(value.to_string().unwrap())
value.to_string().map_err(|err| err.into())
}
}
@@ -33,3 +33,6 @@ features = [
[lib]
crate-type = ["cdylib", "lib"]
[[bin]]
name = "inspect"
@@ -0,0 +1,121 @@
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"
revision_role: u32,
pub revision_role: u32,
}
#[derive(Debug, Clone, Parse)]
@@ -6,7 +6,7 @@ use crate::{
},
shared::exguid::ExGuid,
};
use parser_utils::{errors::Result, log};
use parser_utils::errors::Result;
use std::fmt::Debug;
use std::rc::Rc;
@@ -73,8 +73,9 @@ 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,8 +60,15 @@ impl RevisionManifestList {
let revision = revisions_map.get(&data.base.rid);
if let Some(_revision) = revision {
iterator.next();
// TODO: Find a test .one file that uses this and implement:
log_warn!("TO-DO: Apply the new role and context to the revision");
// 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
);
}
} else {
return Err(
ErrorKind::MalformedOneStoreData("RevisionRoleAndContextDeclarationFND points to a non-existent revision".into()).into()
@@ -1,6 +1,8 @@
use std::rc::Rc;
use crate::onenote::notebook::Notebook;
use crate::onenote::section::{Section, SectionEntry, SectionGroup};
use crate::onestore::{OneStoreType, parse_onestore};
use crate::onestore::{OneStore, OneStoreType, parse_onestore};
use parser_utils::errors::{ErrorKind, Result};
use parser_utils::{fs_driver, log, reader::Reader};
@@ -73,6 +75,11 @@ 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 {
pub trait OneStore: std::fmt::Debug {
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(crate) struct Object {
pub 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 fn id(&self) -> JcId {
pub(crate) fn id(&self) -> JcId {
self.jc_id
}
pub fn props(&self) -> &ObjectPropSet {
pub(crate) fn props(&self) -> &ObjectPropSet {
&self.props
}
@@ -1,6 +1,8 @@
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;
@@ -25,12 +27,41 @@ 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);
// Use the default compact representation of the value
let formatted_value = format!("{:?}", value);
debug_map.entry(&formatted_key, &formatted_value);
let formatted_value = format_value(value);
debug_map.entry(&formatted_key, &DebugOutput::from(formatted_value.as_str()));
}
debug_map.finish()
}
+1
View File
@@ -261,3 +261,4 @@ llamacpp
bgcolor
bordercolor
togglefullscreen
onestore