You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-04-18 19:42:23 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be1a018746 | |||
| ae390469b5 | |||
| c31a7392cc | |||
| 9ca213cefd | |||
| a6a5ab9bc9 | |||
| 0a94d02795 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+9
-2
@@ -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()
|
||||
}
|
||||
|
||||
@@ -261,3 +261,4 @@ llamacpp
|
||||
bgcolor
|
||||
bordercolor
|
||||
togglefullscreen
|
||||
onestore
|
||||
|
||||
Reference in New Issue
Block a user