mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-04 09:20:15 -06:00
88 lines
3.7 KiB
Rust
88 lines
3.7 KiB
Rust
//! File format dispatch by extension. Format crates plug in here.
|
|
|
|
use wordcraft_doc::Document;
|
|
|
|
/// Formats WordCraft opens.
|
|
pub const OPEN_EXTS: &[&str] = &["docx", "docm", "dotx", "txt", "md", "markdown", "html", "htm", "rtf", "odt", "wcraft.json", "json"];
|
|
/// Formats WordCraft saves (Save As).
|
|
pub const SAVE_EXTS: &[&str] = &["docx", "pdf", "txt", "md", "html", "rtf", "odt", "png", "json"];
|
|
|
|
fn ext_of(name: &str) -> String {
|
|
let lower = name.to_ascii_lowercase();
|
|
lower.rsplit('.').next().unwrap_or("").to_string()
|
|
}
|
|
|
|
/// Parse a document from bytes; `name` gives the format by extension.
|
|
pub fn open_bytes(name: &str, bytes: &[u8]) -> Result<Document, String> {
|
|
let ext = ext_of(name);
|
|
let mut doc = match ext.as_str() {
|
|
"txt" | "text" | "" => Document::from_text(&decode_text(bytes)),
|
|
"json" => serde_json::from_slice::<Document>(bytes).map_err(|e| format!("{name}: {e}"))?,
|
|
other => match crate::io_ext::open(other, bytes) {
|
|
Some(r) => r?,
|
|
None => return Err(format!("{name}: unsupported format `.{other}`")),
|
|
},
|
|
};
|
|
doc.ensure_nonempty();
|
|
Ok(doc)
|
|
}
|
|
|
|
/// Serialise a document; `name` gives the format by extension.
|
|
pub fn save_bytes(name: &str, doc: &Document) -> Result<Vec<u8>, String> {
|
|
let ext = ext_of(name);
|
|
match ext.as_str() {
|
|
"txt" | "text" => Ok(doc.plain_text(wordcraft_doc::StoryRef::Body).replace('\n', "\r\n").into_bytes()),
|
|
"json" => serde_json::to_vec_pretty(doc).map_err(|e| e.to_string()),
|
|
other => match crate::io_ext::save(other, doc) {
|
|
Some(r) => r,
|
|
None => Err(format!("{name}: can't save as `.{other}`")),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn open_path(path: &std::path::Path) -> Result<Document, String> {
|
|
let meta = std::fs::metadata(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
|
if meta.len() > 2 << 30 {
|
|
return Err(format!("{}: file is larger than 2 GB", path.display()));
|
|
}
|
|
let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
|
open_bytes(&path.to_string_lossy(), &bytes)
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub fn open_path(path: &std::path::Path) -> Result<Document, String> {
|
|
Err(format!("{}: files are opened through the browser on the web", path.display()))
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn save_path(path: &std::path::Path, doc: &Document) -> Result<(), String> {
|
|
let bytes = save_bytes(&path.to_string_lossy(), doc)?;
|
|
// Write atomically: temp file next to the target, then rename.
|
|
let tmp = path.with_extension(format!("{}.tmp", ext_of(&path.to_string_lossy())));
|
|
std::fs::write(&tmp, &bytes).map_err(|e| format!("{}: {e}", tmp.display()))?;
|
|
std::fs::rename(&tmp, path).map_err(|e| format!("{}: {e}", path.display()))
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub fn save_path(path: &std::path::Path, _doc: &Document) -> Result<(), String> {
|
|
Err(format!("{}: files are saved through the browser on the web", path.display()))
|
|
}
|
|
|
|
/// UTF-8 (with or without BOM), UTF-16 with BOM, else Latin-1.
|
|
pub fn decode_text(b: &[u8]) -> String {
|
|
if let Some(rest) = b.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
|
|
return String::from_utf8_lossy(rest).into_owned();
|
|
}
|
|
if b.len() >= 2 && (b[0] == 0xFF && b[1] == 0xFE || b[0] == 0xFE && b[1] == 0xFF) {
|
|
let le = b[0] == 0xFF;
|
|
let units: Vec<u16> =
|
|
b[2..].as_chunks::<2>().0.iter().map(|c| if le { u16::from_le_bytes([c[0], c[1]]) } else { u16::from_be_bytes([c[0], c[1]]) }).collect();
|
|
return String::from_utf16_lossy(&units);
|
|
}
|
|
match std::str::from_utf8(b) {
|
|
Ok(s) => s.to_string(),
|
|
Err(_) => b.iter().map(|c| *c as char).collect(),
|
|
}
|
|
}
|