diff --git a/README.md b/README.md index d5e843d..b7d6a8f 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,10 @@ snac2nex -s /path/to/snac/storage -t /path/to/nex -u user1 -u user2 --format-filename Post filenames format - * escaped with `%%` - * be careful when removing seconds, as it may cause overwrite collisions + * append trailing slash to parse documents as the directory + * append `.gmi` or `.gemtext` extension to parse documents as the Gemtext + * escape with `%%` when using in `systemd` context + * be careful when removing the seconds part, as it may cause overwrite collisions! [default: %H-%M-%S] diff --git a/src/config.rs b/src/config.rs index 990041d..61845ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,8 +35,10 @@ pub struct Config { /// Post filenames format /// - /// * escaped with `%%` - /// * be careful when removing seconds, as it may cause overwrite collisions + /// * append trailing slash to parse documents as the directory + /// * append `.gmi` or `.gemtext` extension to parse documents as the Gemtext + /// * escape with `%%` when using in `systemd` context + /// * be careful when removing the seconds part, as it may cause overwrite collisions! #[arg(long, default_value_t = String::from("%H-%M-%S"))] pub format_filename: String, diff --git a/src/nex.rs b/src/nex.rs index a74d0b9..38ef32f 100644 --- a/src/nex.rs +++ b/src/nex.rs @@ -1,10 +1,12 @@ mod attachment; +mod format; pub mod response; pub mod source; use anyhow::{Result, bail}; use attachment::Attachment; use chrono::{DateTime, Utc}; +use format::Format; use response::{Clean, Sync, change::Change}; use source::Source; use std::{ @@ -17,10 +19,9 @@ use std::{ pub struct Nex { attachment: Attachment, filename: String, + format: Format, is_cleanup: bool, is_debug: bool, - pattern: String, - time_format: String, users: HashMap, } @@ -34,10 +35,6 @@ impl Nex { (is_cleanup, is_debug): (bool, bool), user_names: &Vec, ) -> Result { - use std::path::MAIN_SEPARATOR; - if filename.contains(MAIN_SEPARATOR) { - bail!("Filename pattern should not contain `{MAIN_SEPARATOR}` characters!") - } // init data export location let target = PathBuf::from_str(&target_dir)?.canonicalize()?; if !target.is_dir() { @@ -51,14 +48,15 @@ impl Nex { fs::create_dir_all(&p)?; users.insert(u.clone(), p); } + // init document format + let format = Format::init(&filename, pattern, time_format); Ok(Self { attachment: Attachment::init(attachment_mode)?, filename, + format, is_cleanup, is_debug, - pattern, - time_format, users, }) } @@ -106,7 +104,9 @@ impl Nex { fs::create_dir_all(&p)?; // init shared post ID once - let id = published.format(&self.filename).to_string(); + let id = published + .format(self.filename.trim_matches('/')) + .to_string(); // init post filepath let mut f = PathBuf::from(&p); @@ -146,77 +146,59 @@ impl Nex { }); } - // format content pattern - let c = self - .pattern - .replace( - "{content}", - &if self.pattern.contains("{tags}") { - content.replace("#", "") - } else { - content - }, - ) - .replace( - "{attachments}", - &attachments - .map(|a| { - let mut b = Vec::with_capacity(a.len()); - b.push("\n".to_string()); - for (n, (name, media_type, source)) in a.into_iter().enumerate() { - let mut t = Vec::with_capacity(3); - t.push(format!( - "=> {}", - match source { - Source::Url(url) => url, - Source::File(from) => { - let to = attachment::filepath(&d, &from, n); - let uri = format!( - "{}/{}", - d.file_name().unwrap().to_string_lossy(), - to.file_name().unwrap().to_string_lossy() - ); - self.attachment.sync(&from, &to).unwrap(); - if let Some(ref mut i) = index { - i.push(to); - } - uri - } - } - )); - if !name.is_empty() { - t.push(name) - } - if !media_type.is_empty() { - t.push(format!("({media_type})")) - } - b.push(t.join(" ")) - } - b.join("\n") - }) - .unwrap_or_default(), - ) - .replace( - "{tags}", - &tags - .map(|t| format!("\n\n{}", t.join(", "))) - .unwrap_or_default(), - ) - .replace("{link}", &format!("\n\n=> {link}")) - .replace( - "{updated}", - &updated - .map(|t| format!("\n\n✏ {}", t.format(&self.time_format))) - .unwrap_or_default(), - ); - // save post file, set its change status let change = if fs::exists(&f)? { Change::Updated } else { Change::Created }; - fs::write(&f, c)?; + fs::write( + &f, + self.format.to_string( + updated, + content, + link, + tags, + attachments.map(|a| { + let mut b = Vec::with_capacity(a.len()); + for (n, (name, media_type, source)) in a.into_iter().enumerate() { + b.push(( + match source { + Source::Url(url) => url, + Source::File(from) => { + let to = attachment::filepath(&d, &from, n); + let uri = format!( + "{}/{}", + d.file_name().unwrap().to_string_lossy(), + to.file_name().unwrap().to_string_lossy() + ); + self.attachment.sync(&from, &to).unwrap(); + if let Some(ref mut i) = index { + i.push(to); + } + uri + } + }, + { + let mut alt = Vec::with_capacity(2); + if !name.is_empty() { + alt.push(name) + } + if !media_type.is_empty() { + alt.push(format!("({media_type})")) + } + if alt.is_empty() { + None + } else { + Some(alt.join(" ")) + } + }, + )) + } + b + }), + ), + )?; // move all paths processed to cleanup ignore if let Some(ref mut i) = index { diff --git a/src/nex/format.rs b/src/nex/format.rs new file mode 100644 index 0000000..7f43ad7 --- /dev/null +++ b/src/nex/format.rs @@ -0,0 +1,110 @@ +use chrono::{DateTime, Utc}; + +pub enum Format { + /// Format as [Gemtext](https://geminiprotocol.net/docs/gemtext.gmi) + /// * detected in case, when the `format_filename` option has `.gmi` or `.gemtext` suffix in pattern + Gemtext { + pattern: String, + time_format: String, + }, + /// It is useful to enable clickable links + /// * detected in case, when the `format_filename` option has trailing slash in pattern + /// * the server should support appending a trailing slash in this case + Dir { + pattern: String, + time_format: String, + }, + /// Ignore markdown + Plain { + pattern: String, + time_format: String, + }, +} + +impl Format { + pub fn init(filename: &str, pattern: String, time_format: String) -> Self { + if filename.ends_with('/') { + Format::Dir { + pattern, + time_format, + } + } else if filename.ends_with(".gmi") | filename.ends_with(".gemtext") { + Format::Gemtext { + pattern, + time_format, + } + } else { + Format::Plain { + pattern, + time_format, + } + } + } + + pub fn to_string( + &self, + updated: Option>, + content: String, + link: String, + tags: Option>, + attachments: Option)>>, + ) -> String { + match self { + Self::Dir { + pattern, + time_format, + } + | Self::Gemtext { + pattern, + time_format, + } + | Self::Plain { + pattern, + time_format, + } => pattern + .replace( + "{content}", + &if pattern.contains("{tags}") { + content.replace("#", "") + } else { + content + }, + ) + .replace( + "{attachments}", + &attachments + .map(|a| { + let mut b = Vec::with_capacity(a.len()); + b.push("\n".to_string()); + for (link, alt) in a { + let mut t = Vec::with_capacity(2); + t.push(if matches!(self, Self::Dir { .. }) { + format!("=> ../{link}") + } else { + format!("=> {link}") + }); + if let Some(text) = alt { + t.push(text) + } + b.push(t.join(" ")) + } + b.join("\n") + }) + .unwrap_or_default(), + ) + .replace( + "{tags}", + &tags + .map(|t| format!("\n\n{}", t.join(", "))) + .unwrap_or_default(), + ) + .replace("{link}", &format!("\n\n=> {link}")) + .replace( + "{updated}", + &updated + .map(|t| format!("\n\n✏ {}", t.format(time_format))) + .unwrap_or_default(), + ), + } // @TODO implement separated templates + } +}