diff --git a/Cargo.lock b/Cargo.lock index 29dab5ca..3f0aec7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,6 +800,7 @@ dependencies = [ "reqwest", "rusqlite", "rusty-hook", + "semver 1.0.7", "serde", "serde_derive", "serde_json", @@ -2154,7 +2155,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.5", + "semver 1.0.7", ] [[package]] @@ -2252,9 +2253,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" [[package]] name = "semver-parser" diff --git a/Cargo.toml b/Cargo.toml index d7cd9b5e..0e8b43d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ intl-memoizer = "0.5.1" sha2 = "0.10" itertools = "0.10.3" mime_guess = "2" +semver = "1.0.7" [dev-dependencies] fbt-lib = "0.1.17" diff --git a/ftd/fpm.ftd b/ftd/fpm.ftd index aa8a2170..0f905635 100644 --- a/ftd/fpm.ftd +++ b/ftd/fpm.ftd @@ -1,5 +1,6 @@ -- record package-data: caption name: +boolean versioned: false optional body about: optional string zip: optional string language: @@ -157,6 +158,10 @@ toc-item list children: +-- toc-item list versions: + + + -- toc-item list language-toc: diff --git a/src/commands/build.rs b/src/commands/build.rs index 0415c59e..8ff2afe4 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -46,33 +46,39 @@ pub async fn build( ); } - match ( - config.package.translation_of.as_ref(), - config.package.translations.has_elements(), - ) { - (Some(_), true) => { - // No package can be both a translation of something and has its own - // translations, when building `config` we ensured this was rejected - unreachable!() - } - (Some(original), false) => { - build_with_original( - config, - original, - file, - base_url, - ignore_failed, - &asset_documents, - ) - .await - } - (None, false) => { - build_simple(config, file, base_url, ignore_failed, &asset_documents).await - } - (None, true) => { - build_with_translations(config, file, base_url, ignore_failed, &asset_documents).await - } - }?; + if config.package.versioned { + fpm::version::build_version(config, file, base_url, ignore_failed, &asset_documents) + .await?; + } else { + match ( + config.package.translation_of.as_ref(), + config.package.translations.has_elements(), + ) { + (Some(_), true) => { + // No package can be both a translation of something and has its own + // translations, when building `config` we ensured this was rejected + unreachable!() + } + (Some(original), false) => { + build_with_original( + config, + original, + file, + base_url, + ignore_failed, + &asset_documents, + ) + .await + } + (None, false) => { + build_simple(config, file, base_url, ignore_failed, &asset_documents).await + } + (None, true) => { + build_with_translations(config, file, base_url, ignore_failed, &asset_documents) + .await + } + }?; + } for dep in dependencies { let static_files = std::collections::BTreeMap::from_iter( @@ -162,6 +168,7 @@ async fn build_with_translations( base_url, skip_failed, asset_documents, + None, ) .await?; } @@ -316,6 +323,7 @@ async fn process_files( base_url, skip_failed, asset_documents, + None, ) .await? } @@ -333,6 +341,7 @@ pub(crate) async fn process_file( base_url: &str, skip_failed: bool, asset_documents: &std::collections::HashMap, + original_id: Option, ) -> fpm::Result<()> { use std::io::Write; @@ -368,7 +377,7 @@ pub(crate) async fn process_file( } } (fpm::File::Static(main_sa), fpm::File::Static(_)) => { - process_static(main_sa, &config.root, package).await? + process_static(main_sa, &config.root, package, original_id).await? } (fpm::File::Code(main_doc), fpm::File::Code(fallback_doc)) => { process_static( @@ -378,6 +387,7 @@ pub(crate) async fn process_file( }, &config.root, package, + original_id, ) .await?; let resp = process_code( @@ -402,7 +412,7 @@ pub(crate) async fn process_file( } } (fpm::File::Image(main_doc), fpm::File::Image(fallback_doc)) => { - process_static(main_doc, &config.root, package).await?; + process_static(main_doc, &config.root, package, original_id).await?; let resp = process_image( config, main_doc, @@ -491,7 +501,7 @@ pub(crate) async fn process_file( } } } - fpm::File::Static(sa) => process_static(sa, &config.root, package).await?, + fpm::File::Static(sa) => process_static(sa, &config.root, package, original_id).await?, fpm::File::Markdown(doc) => { let resp = process_markdown( config, @@ -515,7 +525,7 @@ pub(crate) async fn process_file( } } fpm::File::Image(main_doc) => { - process_static(main_doc, &config.root, package).await?; + process_static(main_doc, &config.root, package, original_id).await?; let resp = process_image( config, main_doc, @@ -546,6 +556,7 @@ pub(crate) async fn process_file( }, &config.root, package, + original_id, ) .await?; let resp = process_code( @@ -1218,10 +1229,11 @@ async fn process_static( sa: &fpm::Static, base_path: &camino::Utf8Path, package: &fpm::Package, + original_id: Option, ) -> fpm::Result<()> { - copy_to_build(sa, base_path, package)?; + copy_to_build(sa, base_path, package, &original_id)?; if let Some(original_package) = package.translation_of.as_ref() { - copy_to_build(sa, base_path, original_package)?; + copy_to_build(sa, base_path, original_package, &original_id)?; } return Ok(()); @@ -1229,17 +1241,24 @@ async fn process_static( sa: &fpm::Static, base_path: &camino::Utf8Path, package: &fpm::Package, + original_id: &Option, ) -> fpm::Result<()> { let build_path = base_path .join(".build") .join("-") .join(package.name.as_str()); + let original_id = if let Some(id) = original_id { + id.as_str() + } else { + sa.id.as_str() + }; + std::fs::create_dir_all(&build_path)?; if let Some((dir, _)) = sa.id.rsplit_once(std::path::MAIN_SEPARATOR) { std::fs::create_dir_all(&build_path.join(dir))?; } std::fs::copy( - sa.base_path.join(sa.id.as_str()), + sa.base_path.join(original_id), build_path.join(sa.id.as_str()), )?; diff --git a/src/config.rs b/src/config.rs index 87f84065..0be23692 100644 --- a/src/config.rs +++ b/src/config.rs @@ -334,15 +334,87 @@ impl Config { Ok(()) } - pub(crate) async fn get_files(&self, package: &fpm::Package) -> fpm::Result> { - let path = if let Some(package_fpm_path) = &package.fpm_path { + pub(crate) async fn get_versions( + &self, + package: &fpm::Package, + ) -> fpm::Result>> { + let path = self.get_root_for_package(package); + let mut ignore_paths = ignore::WalkBuilder::new(&path); + ignore_paths.overrides(fpm::file::package_ignores(package, &path)?); + + let mut hash: std::collections::HashMap> = + std::collections::HashMap::new(); + + let all_files = ignore_paths + .build() + .into_iter() + .flatten() + .map(|x| camino::Utf8PathBuf::from_path_buf(x.into_path()).unwrap()) //todo: improve error message + .collect::>(); + + for file in all_files { + if file.is_dir() { + continue; + } + let version = get_version(&file, &path)?; + let file = fpm::get_file( + package.name.to_string(), + &file, + &(if version.original.eq("BASE_VERSION") { + path.to_owned() + } else { + path.join(&version.original) + }), + ) + .await?; + if let Some(files) = hash.get_mut(&version) { + files.push(file) + } else { + hash.insert(version, vec![file]); + } + } + return Ok(hash); + + fn get_version( + x: &camino::Utf8PathBuf, + path: &camino::Utf8PathBuf, + ) -> fpm::Result { + let id = match std::fs::canonicalize(x)?.to_str().unwrap().rsplit_once( + if path.as_str().ends_with(std::path::MAIN_SEPARATOR) { + path.as_str().to_string() + } else { + format!("{}{}", path, std::path::MAIN_SEPARATOR) + } + .as_str(), + ) { + Some((_, id)) => id.to_string(), + None => { + return Err(fpm::Error::UsageError { + message: format!("{:?} should be a file", x), + }); + } + }; + if let Some((v, _)) = id.split_once('/') { + fpm::Version::parse(v) + } else { + Ok(fpm::Version::base()) + } + } + } + + pub(crate) fn get_root_for_package(&self, package: &fpm::Package) -> camino::Utf8PathBuf { + if let Some(package_fpm_path) = &package.fpm_path { // TODO: Unwrap? package_fpm_path.parent().unwrap().to_owned() } else if package.name.eq(&self.package.name) { self.root.clone() } else { self.packages_root.clone().join(package.name.as_str()) - }; + } + } + + pub(crate) async fn get_files(&self, package: &fpm::Package) -> fpm::Result> { + let path = self.get_root_for_package(package); let mut ignore_paths = ignore::WalkBuilder::new(&path); // ignore_paths.hidden(false); // Allow the linux hidden files to be evaluated ignore_paths.overrides(fpm::file::package_ignores(package, &path)?); @@ -473,6 +545,7 @@ pub(crate) fn find_root_for_file( #[derive(serde::Deserialize, Debug, Clone)] pub(crate) struct PackageTemp { pub name: String, + pub versioned: bool, #[serde(rename = "translation-of")] pub translation_of: Option, #[serde(rename = "translation")] @@ -502,6 +575,7 @@ impl PackageTemp { fpm::Package { name: self.name, + versioned: self.versioned, translation_of: Box::new(translation_of), translations, language: self.language, @@ -522,6 +596,7 @@ impl PackageTemp { #[derive(Debug, Clone)] pub struct Package { pub name: String, + pub versioned: bool, pub translation_of: Box>, pub translations: Vec, pub language: Option, @@ -541,7 +616,6 @@ pub struct Package { /// /// Note that this too is kind of bad design, we will move fonts to `fpm::Package` struct soon. pub fonts: Vec, - pub import_auto_imports_from_original: bool, } @@ -549,6 +623,7 @@ impl Package { pub fn new(name: &str) -> fpm::Package { fpm::Package { name: name.to_string(), + versioned: false, translation_of: Box::new(None), translations: vec![], language: None, diff --git a/src/file.rs b/src/file.rs index 6a446bde..dff0a65b 100644 --- a/src/file.rs +++ b/src/file.rs @@ -17,6 +17,15 @@ impl File { Self::Image(a) => a.id.clone(), } } + pub fn set_id(&mut self, new_id: &str) { + *(match self { + Self::Ftd(a) => &mut a.id, + Self::Static(a) => &mut a.id, + Self::Markdown(a) => &mut a.id, + Self::Code(a) => &mut a.id, + Self::Image(a) => &mut a.id, + }) = new_id.to_string(); + } pub fn get_base_path(&self) -> String { match self { Self::Ftd(a) => a.parent_path.to_string(), diff --git a/src/lib.rs b/src/lib.rs index 49d71660..c5f4e3d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ mod render; mod snapshot; mod tracker; mod translation; +mod version; pub(crate) use auto_import::AutoImport; pub(crate) use commands::build::process_file; @@ -38,6 +39,7 @@ pub(crate) use snapshot::Snapshot; pub(crate) use tracker::Track; pub(crate) use translation::{TranslatedDocument, TranslationData}; pub(crate) use utils::{copy_dir_all, get_timestamp_nanosecond}; +pub(crate) use version::Version; pub const PACKAGE_INFO_INTERFACE: &str = "fifthtry.github.io/package-info"; pub const PACKAGE_THEME_INTERFACE: &str = "fifthtry.github.io/theme"; diff --git a/src/library/get_version_data.rs b/src/library/get_version_data.rs new file mode 100644 index 00000000..342ffd17 --- /dev/null +++ b/src/library/get_version_data.rs @@ -0,0 +1,94 @@ +use itertools::Itertools; + +pub fn processor( + section: &ftd::p1::Section, + doc: &ftd::p2::TDoc, + config: &fpm::Config, + document_id: &str, + base_url: &str, +) -> ftd::p1::Result { + let versions = + futures::executor::block_on(config.get_versions(&config.package)).map_err(|e| { + ftd::p1::Error::ParseError { + message: format!("Cant find versions: {:?}", e), + doc_id: doc.name.to_string(), + line_number: section.line_number, + } + })?; + + let version = if let Some((v, _)) = document_id.split_once('/') { + fpm::Version::parse(v).map_err(|e| ftd::p1::Error::ParseError { + message: format!("{:?}", e), + doc_id: doc.name.to_string(), + line_number: section.line_number, + })? + } else { + fpm::Version::base() + }; + + let doc_id = if let Some(doc) = document_id.split_once('/').map(|(_, v)| v) { + doc + } else { + document_id + } + .to_string(); + + let base_url = base_url + .trim_end_matches('/') + .trim_start_matches('/') + .to_string(); + let base_url = if !base_url.is_empty() { + format!("/{base_url}/") + } else { + String::from("/") + }; + + let url = match doc_id.as_str().rsplit_once('.') { + Some(("index", "ftd")) => base_url, + Some((file_path, "ftd")) | Some((file_path, "md")) => { + format!("{base_url}{file_path}/") + } + Some(_) | None => { + // Unknown file found, create URL + format!("{base_url}{file_path}/", file_path = doc_id.as_str()) + } + }; + let mut found = false; + if let Some(doc) = versions.get(&fpm::Version::base()) { + if doc.iter().map(|v| v.get_id()).any(|x| x == doc_id) { + found = true; + } + } + + let mut version_toc = vec![]; + for key in versions.keys().sorted() { + if key.eq(&fpm::Version::base()) { + continue; + } + let doc = versions[key].to_owned(); + if !found { + if !doc.iter().map(|v| v.get_id()).any(|x| x == doc_id) { + continue; + } + found = true; + } + version_toc.push(fpm::library::toc::TocItem { + id: None, + title: Some(key.original.to_string()), + url: Some(format!("{}{}", key.original, url)), + number: vec![], + is_heading: version.eq(key), + is_disabled: false, + img_src: None, + font_icon: None, + children: vec![], + }); + } + + let toc_items = version_toc + .iter() + .map(|item| item.to_toc_item_compat()) + .collect::>(); + + doc.from_json(&toc_items, section) +} diff --git a/src/library/mod.rs b/src/library/mod.rs index bfa25a40..653f426c 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -1,5 +1,6 @@ mod fpm_dot_ftd; mod get_data; +mod get_version_data; mod http; mod include; mod sqlite; @@ -14,7 +15,6 @@ pub struct Library { /// Hashmap that contains the information about the assets document for the current build /// It'll contain a map of corresponding to the asset doc for that package pub asset_documents: std::collections::HashMap, - pub base_url: String, } @@ -170,13 +170,7 @@ impl ftd::p2::Library for Library { package: &fpm::Package, lib: &Library, ) -> Option { - let path = if let Some(package_fpm_path) = &package.fpm_path { - package_fpm_path.parent()?.to_owned() - } else if package.name.eq(&lib.config.package.name) { - lib.config.root.clone() - } else { - lib.config.packages_root.clone().join(package.name.as_str()) - }; + let path = lib.config.get_root_for_package(package); // Explicit check for the current package. if name.starts_with(&package.name.as_str()) { let new_name = name.replacen(&package.name.as_str(), "", 1); @@ -211,6 +205,13 @@ impl ftd::p2::Library for Library { "toc" => fpm::library::toc::processor(section, doc, &self.config), "include" => fpm::library::include::processor(section, doc, &self.config), "get-data" => fpm::library::get_data::processor(section, doc, &self.config), + "get-version-data" => fpm::library::get_version_data::processor( + section, + doc, + &self.config, + self.document_id.as_str(), + self.base_url.as_str(), + ), t => unimplemented!("$processor$: {} is not implemented yet", t), } } diff --git a/src/translation.rs b/src/translation.rs index ae8c0340..a8c7f8f4 100644 --- a/src/translation.rs +++ b/src/translation.rs @@ -96,6 +96,7 @@ impl TranslatedDocument { base_url, skip_failed, asset_documents, + None, ) .await?; return Ok(()); diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 00000000..56c8137a --- /dev/null +++ b/src/version.rs @@ -0,0 +1,160 @@ +use itertools::Itertools; +use std::cmp::Ordering; +use std::io::Write; + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct Version { + pub major: u64, + pub minor: Option, + pub original: String, +} + +impl Version { + pub(crate) fn base() -> fpm::Version { + fpm::Version { + major: 0, + minor: None, + original: "BASE_VERSION".to_string(), + } + } + + pub(crate) fn parse(s: &str) -> fpm::Result { + let v = s.strip_prefix(&['v', 'V']).unwrap_or(s); + let mut minor = None; + let major = if let Some((major, minor_)) = v.split_once('.') { + if minor_.contains('.') { + return Err(fpm::Error::UsageError { + message: format!("Cannot have more than one dots `.`, found: `{}`", s), + }); + } + let minor_ = minor_.parse::().map_err(|e| fpm::Error::UsageError { + message: format!("Invalid minor for `{}`: `{:?}`", s, e), + })?; + minor = Some(minor_); + major.parse::().map_err(|e| fpm::Error::UsageError { + message: format!("Invalid major for `{}`: `{:?}`", s, e), + })? + } else { + v.parse::().map_err(|e| fpm::Error::UsageError { + message: format!("Invalid major for `{}`: `{:?}`", s, e), + })? + }; + Ok(fpm::Version { + major, + minor, + original: s.to_string(), + }) + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, rhs: &Self) -> Option { + Some(Ord::cmp(self, rhs)) + } +} + +impl Ord for Version { + fn cmp(&self, rhs: &Self) -> std::cmp::Ordering { + if self.major.eq(&rhs.major) { + let lhs_minor = self.minor.unwrap_or(0); + let rhs_minor = rhs.minor.unwrap_or(0); + return lhs_minor.cmp(&rhs_minor); + } + self.major.cmp(&rhs.major) + } +} + +pub(crate) async fn build_version( + config: &fpm::Config, + _file: Option<&str>, + base_url: &str, + skip_failed: bool, + asset_documents: &std::collections::HashMap, +) -> fpm::Result<()> { + let versioned_documents = config.get_versions(&config.package).await?; + let mut documents = std::collections::BTreeMap::new(); + for key in versioned_documents.keys().sorted() { + let doc = versioned_documents[key].to_owned(); + documents.extend( + doc.iter() + .map(|v| (v.get_id(), (key.original.to_string(), v.to_owned()))), + ); + if key.eq(&fpm::Version::base()) { + continue; + } + for (version, doc) in documents.values() { + let mut doc = doc.clone(); + let id = doc.get_id(); + if id.eq("FPM.ftd") { + continue; + } + let new_id = format!("{}/{}", key.original, id); + if !key.original.eq(version) && !fpm::Version::base().original.eq(version) { + if let fpm::File::Ftd(_) = doc { + let original_id = format!("{}/{}", version, id); + let original_file_rel_path = if original_id.contains("index.ftd") { + original_id.replace("index.ftd", "index.html") + } else { + original_id.replace( + ".ftd", + format!("{}index.html", std::path::MAIN_SEPARATOR).as_str(), + ) + }; + let original_file_path = + config.root.join(".build").join(original_file_rel_path); + let file_rel_path = if new_id.contains("index.ftd") { + new_id.replace("index.ftd", "index.html") + } else { + new_id.replace( + ".ftd", + format!("{}index.html", std::path::MAIN_SEPARATOR).as_str(), + ) + }; + let new_file_path = config.root.join(".build").join(file_rel_path); + let original_content = std::fs::read_to_string(&original_file_path)?; + std::fs::create_dir_all(&new_file_path.as_str().replace("index.html", ""))?; + let mut f = std::fs::File::create(&new_file_path)?; + let from_pattern = format!("", base_url, version); + let to_pattern = format!("", base_url, key.original); + f.write_all( + original_content + .replace(from_pattern.as_str(), to_pattern.as_str()) + .as_bytes(), + )?; + continue; + } + } + doc.set_id(new_id.as_str()); + fpm::process_file( + config, + &config.package, + &doc, + None, + None, + Default::default(), + format!("{}{}/", base_url, key.original).as_str(), + skip_failed, + asset_documents, + Some(id), + ) + .await?; + } + } + + for (_, doc) in documents.values() { + fpm::process_file( + config, + &config.package, + doc, + None, + None, + Default::default(), + base_url, + skip_failed, + asset_documents, + None, + ) + .await?; + } + Ok(()) +}