1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00

Chore: OneNote converter: Refactor to allow debugging the import process, reduce use of "unsafe" (#13300)

This commit is contained in:
Henry Heino
2025-09-30 09:03:38 -07:00
committed by GitHub
parent 0b082a985b
commit 14b56f19df
21 changed files with 413 additions and 223 deletions

View File

@@ -1,5 +1,6 @@
/target /target
/output /output
/test-output
/.idea /.idea
*.iml *.iml

View File

@@ -39,4 +39,4 @@ features = [
] ]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib", "lib"]

View File

@@ -43,6 +43,8 @@ After this, the HTML should look the same and is ready to be imported by the Imp
- package.json -> where the project is built - package.json -> where the project is built
- node_functions.js -> where the custom-made functions used inside rust goes - node_functions.js -> where the custom-made functions used inside rust goes
... ...
- tests -> Integration tests
...
- pkg -> artifact folder generated in the build step - pkg -> artifact folder generated in the build step
- onenote_converter.js -> main file - onenote_converter.js -> main file
... ...
@@ -58,7 +60,7 @@ To work with the project you will need:
### Running tests ### Running tests
Tests for the project are located in the `lib` packages, but to make it work it is necessary to build this project first: Most tests for the project are located in the `lib` packages, but to make it work it is necessary to build this project first:
`IS_CONTINUOUS_INTEGRATION=1 yarn build # for production build` `IS_CONTINUOUS_INTEGRATION=1 yarn build # for production build`
or or
@@ -71,6 +73,21 @@ cd ../lib
IS_CONTINUOUS_INTEGRATION=1 yarn test services/interop/InteropService_Importer_OneNote.test. IS_CONTINUOUS_INTEGRATION=1 yarn test services/interop/InteropService_Importer_OneNote.test.
``` ```
Other tests are written in Rust. To run these tests, use `cargo test`:
```
cd packages/onenote-converter
cargo test
```
### Debugging tests
Suppose that the importer's Rust code is failing to parse a specific `example.one` file. In this case, it may be useful to step through part of the import process in a debugger. If using VSCode, this can be done by:
1. Adding a new test to `tests/convert.rs` that runs `convert()` on the `example.one` file.
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.
### Developing ### Developing
When working with the Rust code you will probably rather run `yarn buildDev` since it is faster and it has more logging messages (they can be disabled in the macro `log!()`) When working with the Rust code you will probably rather run `yarn buildDev` since it is faster and it has more logging messages (they can be disabled in the macro `log!()`)

View File

@@ -23,16 +23,6 @@ function removePrefix(basePath, prefix) {
return basePath.replace(prefix, ''); return basePath.replace(prefix, '');
} }
function getOutputPath(inputDir, outputDir, filePath) {
const basePathFromInputFolder = filePath.replace(inputDir, '');
const newOutput = path.join(outputDir, basePathFromInputFolder);
return path.dirname(newOutput);
}
function getParentDir(filePath) {
return path.basename(path.dirname(filePath));
}
function normalizeAndWriteFile(filePath, data) { function normalizeAndWriteFile(filePath, data) {
filePath = path.normalize(filePath); filePath = path.normalize(filePath);
fs.writeFileSync(filePath, data); fs.writeFileSync(filePath, data);
@@ -43,7 +33,5 @@ module.exports = {
isDirectory, isDirectory,
readDir, readDir,
removePrefix, removePrefix,
getOutputPath,
getParentDir,
normalizeAndWriteFile, normalizeAndWriteFile,
}; };

View File

@@ -1,11 +1,9 @@
pub use crate::parser::Parser; pub use crate::parser::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::{eyre, Result};
use color_eyre::eyre::Result;
use std::panic; use std::panic;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use crate::utils::utils::{log, log_warn}; use crate::utils::{fs_driver, utils::{log, log_warn}};
use crate::utils::{get_file_extension, get_file_name, get_output_path, get_parent_dir};
mod notebook; mod notebook;
mod page; mod page;
@@ -37,14 +35,11 @@ fn _main(input_path: &str, output_dir: &str, base_path: &str) -> Result<()> {
pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> { pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
let mut parser = Parser::new(); let mut parser = Parser::new();
let extension: String = unsafe { get_file_extension(path) } let extension: String = fs_driver().get_file_extension(path);
.unwrap()
.as_string()
.unwrap();
match extension.as_str() { match extension.as_str() {
".one" => { ".one" => {
let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap(); let _name: String = fs_driver().get_file_name(path).expect("Missing file name");
log!("Parsing .one file: {}", _name); log!("Parsing .one file: {}", _name);
if path.contains("OneNote_RecycleBin") { if path.contains("OneNote_RecycleBin") {
@@ -53,29 +48,24 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
let section = parser.parse_section(path.to_owned())?; let section = parser.parse_section(path.to_owned())?;
let section_output_dir = unsafe { get_output_path(base_path, output_dir, path) } let section_output_dir = fs_driver().get_output_path(base_path, output_dir, path);
.unwrap()
.as_string()
.unwrap();
section::Renderer::new().render(&section, section_output_dir.to_owned())?; section::Renderer::new().render(&section, section_output_dir.to_owned())?;
} }
".onetoc2" => { ".onetoc2" => {
let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap(); let _name: String = fs_driver().get_file_name(path).expect("Missing file name");
log!("Parsing .onetoc2 file: {}", _name); log!("Parsing .onetoc2 file: {}", _name);
let notebook = parser.parse_notebook(path.to_owned())?; let notebook = parser.parse_notebook(path.to_owned())?;
let notebook_name = unsafe { get_parent_dir(path) } let notebook_name = fs_driver()
.expect("Input file has no parent folder") .get_parent_dir(path)
.as_string() .expect("Input file has no parent folder");
.expect("Parent folder has no name"); if notebook_name == "" {
panic!("Parent directory has no name");
}
log!("notebook name: {:?}", notebook_name); log!("notebook name: {:?}", notebook_name);
let notebook_output_dir = unsafe { get_output_path(base_path, output_dir, path) } let notebook_output_dir = fs_driver().get_output_path(base_path, output_dir, path);
.unwrap()
.as_string()
.unwrap();
log!("Notebok directory: {:?}", notebook_output_dir); log!("Notebok directory: {:?}", notebook_output_dir);
notebook::Renderer::new().render(&notebook, &notebook_name, &notebook_output_dir)?; notebook::Renderer::new().render(&notebook, &notebook_name, &notebook_output_dir)?;
@@ -85,3 +75,4 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
Ok(()) Ok(())
} }

View File

@@ -2,8 +2,8 @@ use crate::parser::notebook::Notebook;
use crate::parser::property::common::Color; use crate::parser::property::common::Color;
use crate::parser::section::{Section, SectionEntry}; use crate::parser::section::{Section, SectionEntry};
use crate::templates::notebook::Toc; use crate::templates::notebook::Toc;
use crate::utils::fs_driver;
use crate::utils::utils::log; use crate::utils::utils::log;
use crate::utils::{join_path, make_dir, remove_prefix};
use crate::{section, templates}; use crate::{section, templates};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use palette::rgb::Rgb; use palette::rgb::Rgb;
@@ -20,12 +20,12 @@ impl Renderer {
pub fn render(&mut self, notebook: &Notebook, name: &str, output_dir: &str) -> Result<()> { pub fn render(&mut self, notebook: &Notebook, name: &str, output_dir: &str) -> Result<()> {
log!("Notebook name: {:?} {:?}", name, output_dir); log!("Notebook name: {:?} {:?}", name, output_dir);
let _ = unsafe { make_dir(output_dir) }; fs_driver().make_dir(output_dir)?;
// let notebook_dir = unsafe { join_path(output_dir, sanitize_filename::sanitize(name).as_str()) }.unwrap().as_string().unwrap(); // let notebook_dir = unsafe { join_path(output_dir, sanitize_filename::sanitize(name).as_str()) }.unwrap().as_string().unwrap();
let notebook_dir = output_dir.to_owned(); let notebook_dir = output_dir.to_owned();
let _ = unsafe { make_dir(&notebook_dir) }; fs_driver().make_dir(&notebook_dir)?;
let mut toc = Vec::new(); let mut toc = Vec::new();
@@ -41,13 +41,10 @@ impl Renderer {
SectionEntry::SectionGroup(group) => { SectionEntry::SectionGroup(group) => {
let dir_name = sanitize_filename::sanitize(group.display_name()); let dir_name = sanitize_filename::sanitize(group.display_name());
let section_group_dir = let section_group_dir =
unsafe { join_path(notebook_dir.as_str(), dir_name.as_str()) } fs_driver().join(notebook_dir.as_str(), dir_name.as_str());
.unwrap()
.as_string()
.unwrap();
log!("Section group directory: {:?}", section_group_dir); log!("Section group directory: {:?}", section_group_dir);
let _ = unsafe { make_dir(section_group_dir.as_str()) }; fs_driver().make_dir(section_group_dir.as_str())?;
let mut entries = Vec::new(); let mut entries = Vec::new();
@@ -84,10 +81,7 @@ impl Renderer {
let section_path = renderer.render(section, notebook_dir)?; let section_path = renderer.render(section, notebook_dir)?;
log!("section_path: {:?}", section_path); log!("section_path: {:?}", section_path);
let path_from_base_dir = unsafe { remove_prefix(section_path, base_dir.as_str()) } let path_from_base_dir = String::from(fs_driver().remove_prefix(&section_path, &base_dir));
.unwrap()
.as_string()
.unwrap();
log!("path_from_base_dir: {:?}", path_from_base_dir); log!("path_from_base_dir: {:?}", path_from_base_dir);
Ok(templates::notebook::Section { Ok(templates::notebook::Section {
name: section.display_name().to_string(), name: section.display_name().to_string(),

View File

@@ -1,8 +1,8 @@
use crate::page::Renderer; use crate::page::Renderer;
use crate::parser::contents::EmbeddedFile; use crate::parser::contents::EmbeddedFile;
use crate::parser::property::embedded_file::FileType; use crate::parser::property::embedded_file::FileType;
use crate::utils::fs_driver;
use crate::utils::utils::log; use crate::utils::utils::log;
use crate::utils::{join_path, write_file};
use color_eyre::eyre::ContextCompat; use color_eyre::eyre::ContextCompat;
use color_eyre::Result; use color_eyre::Result;
use std::path::PathBuf; use std::path::PathBuf;
@@ -12,12 +12,9 @@ impl<'a> Renderer<'a> {
let content; let content;
let filename = self.determine_filename(file.filename())?; let filename = self.determine_filename(file.filename())?;
let path = unsafe { join_path(self.output.as_str(), filename.as_str()) } let path = fs_driver().join(self.output.as_str(), filename.as_str());
.unwrap()
.as_string()
.unwrap();
log!("Rendering embedded file: {:?}", path); log!("Rendering embedded file: {:?}", path);
let _ = unsafe { write_file(path.as_str(), file.data()) }; fs_driver().write_file(&path, file.data())?;
let file_type = Self::guess_type(file); let file_type = Self::guess_type(file);

View File

@@ -1,7 +1,7 @@
use crate::page::Renderer; use crate::page::Renderer;
use crate::parser::contents::Image; use crate::parser::contents::Image;
use crate::utils::utils::log; use crate::utils::utils::log;
use crate::utils::{join_path, px, write_file, AttributeSet, StyleSet}; use crate::utils::{fs_driver, px, AttributeSet, StyleSet};
use color_eyre::Result; use color_eyre::Result;
impl<'a> Renderer<'a> { impl<'a> Renderer<'a> {
@@ -10,12 +10,9 @@ impl<'a> Renderer<'a> {
if let Some(data) = image.data() { if let Some(data) = image.data() {
let filename = self.determine_image_filename(image)?; let filename = self.determine_image_filename(image)?;
let path = unsafe { join_path(self.output.as_str(), filename.as_str()) } let path = fs_driver().join(self.output.as_str(), filename.as_str());
.unwrap()
.as_string()
.unwrap();
log!("Rendering image: {:?}", path); log!("Rendering image: {:?}", path);
let _ = unsafe { write_file(path.as_str(), data) }; fs_driver().write_file(path.as_str(), data)?;
let mut attrs = AttributeSet::new(); let mut attrs = AttributeSet::new();
let mut styles = StyleSet::new(); let mut styles = StyleSet::new();

View File

@@ -1,4 +1,3 @@
use crate::log_warn;
use crate::parser::errors::{ErrorKind, Result}; use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::cell_id::CellId; use crate::parser::fsshttpb::data::cell_id::CellId;
use crate::parser::fsshttpb::data::exguid::ExGuid; use crate::parser::fsshttpb::data::exguid::ExGuid;
@@ -9,6 +8,7 @@ use crate::parser::one::property::{simple, PropertyType};
use crate::parser::one::property_set::PropertySetId; use crate::parser::one::property_set::PropertySetId;
use crate::parser::onestore::object::Object; use crate::parser::onestore::object::Object;
use crate::parser::shared::guid::Guid; use crate::parser::shared::guid::Guid;
use crate::utils::utils::log_warn;
/// A page series. /// A page series.
/// ///

View File

@@ -4,11 +4,8 @@ use crate::parser::onenote::notebook::Notebook;
use crate::parser::onenote::section::{Section, SectionEntry, SectionGroup}; use crate::parser::onenote::section::{Section, SectionEntry, SectionGroup};
use crate::parser::onestore::parse_store; use crate::parser::onestore::parse_store;
use crate::parser::reader::Reader; use crate::parser::reader::Reader;
use crate::parser::utils::{exists, is_directory, read_dir, read_file}; use crate::utils::fs_driver;
use crate::utils::utils::log; use crate::utils::utils::log;
use crate::utils::{get_dir_name, get_file_extension, get_file_name, join_path};
use std::panic;
use web_sys::js_sys::Uint8Array;
pub(crate) mod content; pub(crate) mod content;
pub(crate) mod embedded_file; pub(crate) mod embedded_file;
@@ -26,7 +23,6 @@ pub(crate) mod rich_text;
pub(crate) mod section; pub(crate) mod section;
pub(crate) mod table; pub(crate) mod table;
extern crate console_error_panic_hook;
extern crate lazy_static; extern crate lazy_static;
/// The OneNote file parser. /// The OneNote file parser.
@@ -35,7 +31,6 @@ pub struct Parser;
impl Parser { impl Parser {
/// Create a new OneNote file parser. /// Create a new OneNote file parser.
pub fn new() -> Parser { pub fn new() -> Parser {
panic::set_hook(Box::new(console_error_panic_hook::hook));
Parser {} Parser {}
} }
@@ -46,9 +41,7 @@ impl Parser {
/// sections from the folder that the table of contents file is in. /// sections from the folder that the table of contents file is in.
pub fn parse_notebook(&mut self, path: String) -> Result<Notebook> { pub fn parse_notebook(&mut self, path: String) -> Result<Notebook> {
log!("Parsing notebook: {:?}", path); log!("Parsing notebook: {:?}", path);
let file_content = unsafe { read_file(path.as_str()) }.unwrap(); let data = fs_driver().read_file(&path)?;
let array = Uint8Array::new(&file_content);
let data = array.to_vec();
let packaging = OneStorePackaging::parse(&mut Reader::new(&data))?; let packaging = OneStorePackaging::parse(&mut Reader::new(&data))?;
let store = parse_store(&packaging)?; let store = parse_store(&packaging)?;
@@ -56,29 +49,20 @@ impl Parser {
return Err(ErrorKind::NotATocFile { file: path }.into()); return Err(ErrorKind::NotATocFile { file: path }.into());
} }
let base_dir = unsafe { get_dir_name(path.as_str()) } let base_dir = fs_driver().get_dir_name(&path);
.expect("base dir not found")
.as_string()
.unwrap();
let sections = notebook::parse_toc(store.data_root())? let sections = notebook::parse_toc(store.data_root())?
.iter() .iter()
.map(|name| { .map(|name| fs_driver().join(&base_dir, name))
let result = unsafe { join_path(base_dir.as_str(), name) }
.unwrap()
.as_string()
.unwrap();
return result;
})
.filter(|p| !p.contains("OneNote_RecycleBin")) .filter(|p| !p.contains("OneNote_RecycleBin"))
.filter(|p| { .filter(|p| {
let is_file = match unsafe { exists(p.as_str()) } { let is_file = match fs_driver().exists(p) {
Ok(is_file) => is_file, Ok(is_file) => is_file,
Err(_err) => false, Err(_err) => false,
}; };
return is_file; return is_file;
}) })
.map(|p| { .map(|p| {
let is_dir = unsafe { is_directory(p.as_str()) }.unwrap(); let is_dir = fs_driver().is_directory(&p)?;
if !is_dir { if !is_dir {
self.parse_section(p).map(SectionEntry::Section) self.parse_section(p).map(SectionEntry::Section)
} else { } else {
@@ -96,9 +80,7 @@ impl Parser {
/// OneNote section. /// OneNote section.
pub fn parse_section(&mut self, path: String) -> Result<Section> { pub fn parse_section(&mut self, path: String) -> Result<Section> {
log!("Parsing section: {:?}", path); log!("Parsing section: {:?}", path);
let file_content = unsafe { read_file(path.as_str()) }.unwrap(); let data = fs_driver().read_file(path.as_str())?;
let array = Uint8Array::new(&file_content);
let data = array.to_vec();
let packaging = OneStorePackaging::parse(&mut Reader::new(&data))?; let packaging = OneStorePackaging::parse(&mut Reader::new(&data))?;
let store = parse_store(&packaging)?; let store = parse_store(&packaging)?;
@@ -106,25 +88,20 @@ impl Parser {
return Err(ErrorKind::NotASectionFile { file: path }.into()); return Err(ErrorKind::NotASectionFile { file: path }.into());
} }
let filename = unsafe { get_file_name(path.as_str()) } let filename = fs_driver()
.expect("file without file name") .get_file_name(&path)
.as_string() .expect("file without file name");
.unwrap();
section::parse_section(store, filename) section::parse_section(store, filename)
} }
fn parse_section_group(&mut self, path: String) -> Result<SectionGroup> { fn parse_section_group(&mut self, path: String) -> Result<SectionGroup> {
let display_name = unsafe { get_file_name(path.as_str()) } let display_name = fs_driver()
.expect("file without file name") .get_file_name(path.as_str())
.as_string() .expect("file without file name");
.unwrap();
if let Some(entries) = unsafe { read_dir(path.as_str()) } { if let Ok(entries) = fs_driver().read_dir(&path) {
for entry in entries { for entry in entries {
let ext = unsafe { get_file_extension(entry.as_str()) } let ext = fs_driver().get_file_extension(&entry);
.unwrap()
.as_string()
.unwrap();
if ext == ".onetoc2" { if ext == ".onetoc2" {
return self.parse_notebook(entry).map(|group| SectionGroup { return self.parse_notebook(entry).map(|group| SectionGroup {
display_name, display_name,

View File

@@ -31,7 +31,7 @@ impl<'a> OneStore<'a> {
self.schema self.schema
} }
pub(crate) fn data_root(&'a self) -> &'a ObjectSpace { pub(crate) fn data_root(&self) -> &ObjectSpace {
&self.data_root &self.data_root
} }

View File

@@ -3,8 +3,6 @@ use itertools::Itertools;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::fmt::Display; use std::fmt::Display;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use widestring::U16CString; use widestring::U16CString;
pub(crate) struct AttributeSet(HashMap<&'static str, String>); pub(crate) struct AttributeSet(HashMap<&'static str, String>);
@@ -69,35 +67,6 @@ impl Display for StyleSet {
} }
} }
#[wasm_bindgen(module = "/node_functions.js")]
extern "C" {
#[wasm_bindgen(js_name = isDirectory, catch)]
pub unsafe fn is_directory(path: &str) -> std::result::Result<bool, JsValue>;
#[wasm_bindgen(js_name = readDir, catch)]
unsafe fn read_dir_js(path: &str) -> std::result::Result<JsValue, JsValue>;
}
pub unsafe fn read_dir(path: &str) -> Option<Vec<String>> {
let result_ptr = read_dir_js(path).unwrap();
let result_str: String = match result_ptr.as_string() {
Some(x) => x,
_ => String::new(),
};
let names: Vec<String> = result_str.split('\n').map(|s| s.to_string()).collect_vec();
Some(names)
}
#[wasm_bindgen(module = "fs")]
extern "C" {
#[wasm_bindgen(js_name = readFileSync, catch)]
pub unsafe fn read_file(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = existsSync, catch)]
pub unsafe fn exists(path: &str) -> std::result::Result<bool, JsValue>;
}
pub(crate) trait Utf16ToString { pub(crate) trait Utf16ToString {
fn utf16_to_string(&self) -> Result<String>; fn utf16_to_string(&self) -> Result<String>;
} }

View File

@@ -1,6 +1,6 @@
use crate::parser::section::Section; use crate::parser::section::Section;
use crate::utils::fs_driver;
use crate::utils::utils::log; use crate::utils::utils::log;
use crate::utils::{join_path, make_dir, remove_prefix, write_file};
use crate::{page, templates}; use crate::{page, templates};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use std::collections::HashSet; use std::collections::HashSet;
@@ -19,15 +19,10 @@ impl Renderer {
} }
pub fn render(&mut self, section: &Section, output_dir: String) -> Result<String> { pub fn render(&mut self, section: &Section, output_dir: String) -> Result<String> {
let section_dir = unsafe { let section_dir = fs_driver().join(
join_path( output_dir.as_str(),
output_dir.as_str(), sanitize_filename::sanitize(section.display_name()).as_str(),
sanitize_filename::sanitize(section.display_name()).as_str(), );
)
}
.unwrap()
.as_string()
.unwrap();
log!( log!(
"section_dir: {:?} \n output_dir: {:?}", "section_dir: {:?} \n output_dir: {:?}",
section_dir, section_dir,
@@ -35,7 +30,7 @@ impl Renderer {
); );
log!("Rendering section: {:?}", section_dir); log!("Rendering section: {:?}", section_dir);
let _ = unsafe { make_dir(section_dir.as_str()) }; fs_driver().make_dir(section_dir.as_str())?;
let mut toc = Vec::new(); let mut toc = Vec::new();
let mut fallback_title_index = 0; let mut fallback_title_index = 0;
@@ -52,38 +47,27 @@ impl Renderer {
let file_name = self.determine_page_filename(&file_name)?; let file_name = self.determine_page_filename(&file_name)?;
let file_name = sanitize_filename::sanitize(file_name + ".html"); let file_name = sanitize_filename::sanitize(file_name + ".html");
let page_path = unsafe { join_path(section_dir.as_str(), file_name.as_str()) } let page_path = fs_driver().join(section_dir.as_str(), file_name.as_str());
.unwrap()
.as_string()
.unwrap();
let mut renderer = page::Renderer::new(section_dir.clone(), self); let mut renderer = page::Renderer::new(section_dir.clone(), self);
let page_html = renderer.render_page(page)?; let page_html = renderer.render_page(page)?;
log!("Creating page file: {:?}", page_path); log!("Creating page file: {:?}", page_path);
let _ = unsafe { write_file(&page_path, page_html.as_bytes()) }; fs_driver().write_file(&page_path, page_html.as_bytes())?;
let page_path_without_basedir = let page_path_without_basedir =
unsafe { remove_prefix(page_path, output_dir.as_str()) } String::from(fs_driver().remove_prefix(&page_path, output_dir.as_str()));
.unwrap()
.as_string()
.unwrap();
toc.push((title, page_path_without_basedir, page.level())) toc.push((title, page_path_without_basedir, page.level()))
} }
} }
let toc_html = templates::section::render(section.display_name(), toc)?; let toc_html = templates::section::render(section.display_name(), toc)?;
let toc_file = unsafe { let toc_file = fs_driver().join(
join_path( output_dir.as_str(),
output_dir.as_str(), format!("{}.html", section.display_name()).as_str(),
format!("{}.html", section.display_name()).as_str(), );
)
}
.unwrap()
.as_string()
.unwrap();
log!("ToC: {:?}", toc_file); log!("ToC: {:?}", toc_file);
let _ = unsafe { write_file(toc_file.as_str(), toc_html.as_bytes()) }; fs_driver().write_file(toc_file.as_str(), toc_html.as_bytes())?;
Ok(section_dir) Ok(section_dir)
} }

View File

@@ -0,0 +1,45 @@
pub type ApiResult<T> = std::result::Result<T, std::io::Error>;
pub trait FileApiDriver: Send + Sync {
fn is_directory(&self, path: &str) -> ApiResult<bool>;
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>>;
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>>;
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()>;
fn make_dir(&self, path: &str) -> ApiResult<()>;
fn exists(&self, path: &str) -> ApiResult<bool>;
// These functions correspond to the similarly-named
// path functions.
fn get_file_name(&self, path: &str) -> Option<String>;
fn get_file_extension(&self, path: &str) -> String;
fn get_dir_name(&self, path: &str) -> String;
fn join(&self, path_1: &str, path_2: &str) -> String;
fn remove_prefix<'a>(&self, full_path: &'a str, prefix: &str) -> &'a str {
if full_path.starts_with(prefix) {
&full_path[prefix.len()..]
} else {
full_path
}
}
fn get_output_path(&self, input_dir: &str, output_dir: &str, file_path: &str) -> String {
let base_path = self.remove_prefix(file_path, input_dir);
let rebased_output = self.join(output_dir, base_path);
self.get_dir_name(&rebased_output)
}
fn get_parent_dir(&self, path: &str) -> Option<String> {
let dir_name = self.get_dir_name(path);
let result = self.get_file_name(&dir_name);
if let Some(value) = result {
if value == "" {
None
} else {
Some(value)
}
} else {
None
}
}
}

View File

@@ -0,0 +1,23 @@
pub mod api;
pub use api::ApiResult;
pub use api::FileApiDriver;
use lazy_static::lazy_static;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
mod wasm_driver;
#[cfg(target_arch = "wasm32")]
use wasm_driver::FileApiDriverImpl;
#[cfg(not(target_arch = "wasm32"))]
mod native_driver;
#[cfg(not(target_arch = "wasm32"))]
use native_driver::FileApiDriverImpl;
lazy_static! {
static ref FS_DRIVER: Arc<dyn FileApiDriver> = Arc::new(FileApiDriverImpl {});
}
pub fn fs_driver() -> Arc<dyn FileApiDriver> {
FS_DRIVER.clone()
}

View File

@@ -0,0 +1,64 @@
use super::ApiResult;
use super::FileApiDriver;
use std::fs;
use std::path::Path;
pub struct FileApiDriverImpl {}
impl FileApiDriver for FileApiDriverImpl {
fn is_directory(&self, path: &str) -> ApiResult<bool> {
let metadata = fs::metadata(path)?;
let file_type = metadata.file_type();
Ok(file_type.is_dir())
}
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>> {
let mut result: Vec<String> = Vec::new();
for item in fs::read_dir(path)? {
let item = item?.path();
result.push(item.to_string_lossy().into())
}
Ok(result)
}
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>> {
Ok(fs::read(path)?)
}
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
Ok(fs::write(path, data)?)
}
fn exists(&self, path: &str) -> ApiResult<bool> {
Ok(fs::exists(path)?)
}
fn make_dir(&self, path: &str) -> ApiResult<()> {
Ok(fs::create_dir(path)?)
}
fn get_file_name(&self, path: &str) -> Option<String> {
match Path::new(path).file_name() {
Some(name) => Some(name.to_string_lossy().into()),
None => None,
}
}
fn get_file_extension(&self, path: &str) -> String {
match Path::new(path).extension() {
Some(ext) => {
let extension = String::from(ext.to_string_lossy());
String::from(".") + &extension
}
None => String::from(""),
}
}
fn get_dir_name(&self, path: &str) -> String {
match Path::new(path).parent() {
Some(parent) => parent.to_string_lossy().into(),
None => String::from(""),
}
}
fn join(&self, path_1: &str, path_2: &str) -> String {
Path::new(path_1).join(path_2).to_string_lossy().into()
}
}

View File

@@ -0,0 +1,140 @@
use super::ApiResult;
use super::FileApiDriver;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use web_sys::js_sys;
use web_sys::js_sys::Uint8Array;
#[wasm_bindgen(module = "/node_functions.js")]
extern "C" {
#[wasm_bindgen(js_name = mkdirSyncRecursive, catch)]
fn make_dir(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = pathSep, catch)]
fn path_sep() -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = removePrefix, catch)]
fn remove_prefix(base_path: String, prefix: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = getOutputPath, catch)]
fn get_output_path(
input_dir: &str,
output_dir: &str,
file_path: &str,
) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = normalizeAndWriteFile, catch)]
fn write_file(path: &str, data: &[u8]) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = isDirectory, catch)]
fn is_directory(path: &str) -> std::result::Result<bool, JsValue>;
#[wasm_bindgen(js_name = readDir, catch)]
fn read_dir_js(path: &str) -> std::result::Result<JsValue, JsValue>;
}
#[wasm_bindgen(module = "fs")]
extern "C" {
#[wasm_bindgen(js_name = readFileSync, catch)]
fn read_file(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = existsSync, catch)]
fn exists(path: &str) -> std::result::Result<bool, JsValue>;
}
#[wasm_bindgen(module = "path")]
extern "C" {
#[wasm_bindgen(js_name = basename, catch)]
fn get_file_name(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = extname, catch)]
fn get_file_extension(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = dirname, catch)]
fn get_dir_name(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = join, catch)]
fn join_path(path_1: &str, path_2: &str) -> std::result::Result<JsValue, JsValue>;
}
fn handle_error(error: JsValue, source: &str) -> std::io::Error {
use std::io::Error;
use std::io::ErrorKind;
let error = js_sys::Error::from(error);
match error.name().to_string() {
_ => Error::new(
ErrorKind::Other,
String::from(format!("Err({}): {:?}", source, error)),
),
}
}
pub struct FileApiDriverImpl {}
impl FileApiDriver for FileApiDriverImpl {
fn is_directory(&self, path: &str) -> ApiResult<bool> {
match is_directory(path) {
Ok(is_dir) => Ok(is_dir),
Err(e) => Err(handle_error(e, "checking is_directory")),
}
}
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>> {
let result_ptr = read_dir_js(path).unwrap();
let result_str: String = match result_ptr.as_string() {
Some(x) => x,
_ => String::new(),
};
Ok(result_str.split('\n').map(|s| s.to_string()).collect())
}
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>> {
match read_file(path) {
Ok(file) => Ok(Uint8Array::new(&file).to_vec()),
Err(e) => Err(handle_error(e, &format!("reading file {}", path))),
}
}
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
if let Err(error) = write_file(path, data) {
Err(handle_error(error, &format!("writing file {}", path)))
} else {
Ok(())
}
}
fn exists(&self, path: &str) -> ApiResult<bool> {
match exists(path) {
Ok(exists) => Ok(exists),
Err(e) => Err(handle_error(e, &format!("checking exists {}", path))),
}
}
fn make_dir(&self, path: &str) -> ApiResult<()> {
if let Err(error) = make_dir(path) {
Err(handle_error(error, &format!("mkdir {}", path)))
} else {
Ok(())
}
}
fn get_file_name(&self, path: &str) -> Option<String> {
let file_name = get_file_name(path).unwrap().as_string().unwrap();
if file_name == "" {
None
} else {
Some(file_name)
}
}
fn get_file_extension(&self, path: &str) -> String {
get_file_extension(path).unwrap().as_string().unwrap()
}
fn get_dir_name(&self, path: &str) -> String {
get_dir_name(path).unwrap().as_string().unwrap()
}
fn join(&self, path_1: &str, path_2: &str) -> String {
join_path(path_1, path_2).unwrap().as_string().unwrap()
}
}

View File

@@ -5,10 +5,12 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::fmt::Display; use std::fmt::Display;
use std::sync::Mutex; use std::sync::Mutex;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use widestring::U16CString; use widestring::U16CString;
mod file_api;
pub use file_api::fs_driver;
pub use file_api::FileApiDriver;
pub(crate) fn px(inches: f32) -> String { pub(crate) fn px(inches: f32) -> String {
format!("{}px", (inches * 48.0).round()) format!("{}px", (inches * 48.0).round())
} }
@@ -74,63 +76,8 @@ impl Display for StyleSet {
} }
} }
#[wasm_bindgen(module = "/node_functions.js")]
extern "C" {
#[wasm_bindgen(js_name = mkdirSyncRecursive, catch)]
pub unsafe fn make_dir(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = pathSep, catch)]
pub unsafe fn path_sep() -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = removePrefix, catch)]
pub unsafe fn remove_prefix(
base_path: String,
prefix: &str,
) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = getOutputPath, catch)]
pub unsafe fn get_output_path(
input_dir: &str,
output_dir: &str,
file_path: &str,
) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = getParentDir, catch)]
pub unsafe fn get_parent_dir(input_dir: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = normalizeAndWriteFile, catch)]
pub unsafe fn write_file(path: &str, data: &[u8]) -> std::result::Result<JsValue, JsValue>;
}
#[wasm_bindgen(module = "fs")]
extern "C" {
// #[wasm_bindgen(js_name = writeFileSync, catch)]
// pub unsafe fn write_file(path: &str, data: &[u8]) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = readFileSync, catch)]
pub unsafe fn read_file(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = existsSync, catch)]
pub unsafe fn exists(path: &str) -> std::result::Result<bool, JsValue>;
}
#[wasm_bindgen(module = "path")]
extern "C" {
#[wasm_bindgen(js_name = basename, catch)]
pub unsafe fn get_file_name(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = extname, catch)]
pub unsafe fn get_file_extension(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = dirname, catch)]
pub unsafe fn get_dir_name(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = join, catch)]
pub unsafe fn join_path(path_1: &str, path_2: &str) -> std::result::Result<JsValue, JsValue>;
}
pub mod utils { pub mod utils {
#[cfg(target_arch = "wasm32")]
macro_rules! log { macro_rules! log {
( $( $t:tt )* ) => { ( $( $t:tt )* ) => {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -138,15 +85,32 @@ pub mod utils {
} }
} }
#[cfg(target_arch = "wasm32")]
macro_rules! log_warn { macro_rules! log_warn {
( $( $t:tt )* ) => { ( $( $t:tt )* ) => {
use crate::utils::get_current_page; use crate::utils::get_current_page;
web_sys::console::warn_1(&format!("OneNoteConverter: Warning around the following page: {}", get_current_page().unwrap()).into()); web_sys::console::warn_1(&format!("OneNoteConverter: Warning around the following page: {}", get_current_page()).into());
web_sys::console::warn_2(&format!("OneNoteConverter: ").into(), &format!( $( $t )* ).into()); web_sys::console::warn_2(&format!("OneNoteConverter: ").into(), &format!( $( $t )* ).into());
} }
} }
#[cfg(not(target_arch = "wasm32"))]
macro_rules! log {
( $( $t:tt )* ) => {
#[cfg(debug_assertions)]
println!( $( $t )* );
}
}
#[cfg(not(target_arch = "wasm32"))]
macro_rules! log_warn {
( $( $t:tt )* ) => {
use crate::utils::get_current_page;
println!("Warning: {}, near {}", &format!( $( $t )* ), get_current_page());
}
}
pub(crate) use log; pub(crate) use log;
pub(crate) use log_warn; pub(crate) use log_warn;
} }
@@ -177,7 +141,9 @@ pub fn set_current_page(page_name: String) {
*current_page = Some(page_name.to_string()); *current_page = Some(page_name.to_string());
} }
pub fn get_current_page() -> Option<String> { pub fn get_current_page() -> String {
let current_page = CURRENT_PAGE.lock().unwrap(); let current_page = CURRENT_PAGE.lock().unwrap();
current_page.clone() current_page
.clone()
.unwrap_or_else(|| String::from("[None]"))
} }

View File

@@ -0,0 +1,37 @@
use onenote_converter::convert;
use std::fs;
use std::path::PathBuf;
fn get_output_dir() -> PathBuf {
PathBuf::from("./test-output")
}
fn setup() {
let output_dir = get_output_dir();
if output_dir.exists() {
fs::remove_dir_all(&output_dir).unwrap();
}
fs::create_dir(&output_dir).unwrap();
}
#[test]
fn convert_simple() {
setup();
let output_dir = get_output_dir();
convert(
"./assets/test-data/single-page/Untitled Section.one",
&output_dir.to_string_lossy(),
"./assets/test-data/single-page/",
)
.unwrap();
// Should create a table of contents file
assert!(output_dir.join("Untitled Section.html").exists());
// Should convert the input page to an HTML file
assert!(output_dir
.join("Untitled Section")
.join("test.html")
.exists());
}