From a7175374dc087372dffca0ee517fe48fb5d276f5 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Fri, 22 Nov 2024 21:00:11 +0100 Subject: [PATCH] feat(generics): use config for generic content and some spas --- Cargo.lock | 2 - crates/rari-cli/main.rs | 8 +- crates/rari-doc/Cargo.toml | 2 - crates/rari-doc/src/cached_readers.rs | 100 +++++++ crates/rari-doc/src/error.rs | 2 + crates/rari-doc/src/pages/build.rs | 10 +- crates/rari-doc/src/pages/types/generic.rs | 58 ++-- crates/rari-doc/src/pages/types/spa.rs | 319 ++++++++++++--------- 8 files changed, 324 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d50053f1..fda12a40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2662,8 +2662,6 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "phf 0.11.2", - "phf_macros 0.11.2", "pretty_yaml", "rari-data", "rari-md", diff --git a/crates/rari-cli/main.rs b/crates/rari-cli/main.rs index e4b0865f..9b10ee04 100644 --- a/crates/rari-cli/main.rs +++ b/crates/rari-cli/main.rs @@ -164,6 +164,8 @@ struct BuildArgs { #[arg(long)] skip_spas: bool, #[arg(long)] + skip_generics: bool, + #[arg(long)] skip_sitemap: bool, #[arg(long)] templ_stats: bool, @@ -280,9 +282,13 @@ fn main() -> Result<(), Error> { if !args.skip_spas && args.files.is_empty() { let start = std::time::Instant::now(); urls.extend(build_spas()?); - urls.extend(build_generic_pages()?); println!("Took: {: >10.3?} to build spas", start.elapsed()); } + if !args.skip_generics && args.files.is_empty() { + let start = std::time::Instant::now(); + urls.extend(build_generic_pages()?); + println!("Took: {: >10.3?} to build generics", start.elapsed()); + } if !args.skip_content { let start = std::time::Instant::now(); urls.extend(build_docs(&docs)?); diff --git a/crates/rari-doc/Cargo.toml b/crates/rari-doc/Cargo.toml index ff20a1ba..a43da182 100644 --- a/crates/rari-doc/Cargo.toml +++ b/crates/rari-doc/Cargo.toml @@ -52,8 +52,6 @@ strum = { version = "0.26", features = ["derive"] } imagesize = "0.13" svg_metadata = "0.5" memoize = "0.4" -phf_macros = "0.11" -phf = "0.11" unescaper = "0.1" diff --git a/crates/rari-doc/src/cached_readers.rs b/crates/rari-doc/src/cached_readers.rs index 8491e92b..ec99bba6 100644 --- a/crates/rari-doc/src/cached_readers.rs +++ b/crates/rari-doc/src/cached_readers.rs @@ -35,6 +35,7 @@ use rari_types::globals::{ use rari_types::locale::Locale; use rari_utils::concat_strs; use rari_utils::io::read_to_string; +use serde::{Deserialize, Serialize}; use tracing::error; use crate::contributors::{WikiHistories, WikiHistory}; @@ -517,6 +518,77 @@ pub fn read_and_cache_doc_pages() -> Result, DocError> { /// A type alias for a hashmap that maps URLs to pages. pub type UrlToPageMap = HashMap; +/// Represents the configuration for generic pages. +/// +/// # Fields +/// +/// * `slug_prefix` - The prefix for the slug, defaults to None. +/// +/// * `title_suffix` - A suffix to add to the page title. +#[derive(Debug, Clone, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct GenericPagesConfig { + pub slug_prefix: Option, + pub title_suffix: Option, +} +/// Represents the configuration for all generic content. +/// +/// # Fields +/// +/// * `pages` - A `HashMap` mapping pages sections to their +/// according `GenericPagesConfig`. +/// +/// * `title_suffix` - A suffix to add to the page title. +#[derive(Debug, Clone, Deserialize, Default)] +pub struct GenericContentConfig { + pub pages: HashMap, + pub spas: HashMap, +} + +static GENERIC_CONTENT_CONFIG: OnceLock = OnceLock::new(); + +fn read_generic_content_config() -> Result { + if let Some(root) = generic_content_root() { + let json_str = read_to_string(root.join("config.json"))?; + let config: GenericContentConfig = serde_json::from_str(&json_str)?; + Ok(config) + } else { + Err(DocError::NoGenericContentConfig) + } +} + +/// Provides access to the generic content configuration. +/// +/// This function returns the generic content configuration, either from a +/// cached, lazily-initialized global value (`GENERIC_CONTENT_CONFIG`) or by +/// re-reading the configuration file, depending on the value of `cache_content()`. +/// +/// - If `cache_content()` is true, the configuration is cached and re-used +/// across calls. +/// - If `cache_content()` is false, the configuration is re-read from the +/// `config.json` file on each call. +/// +/// Any errors encountered during the reading or parsing of the configuration +/// are logged, and a default configuration is returned. +/// +/// # Returns +/// A `Cow<'static, GenericContentConfig>` representing the configuration. +/// - If the configuration is cached, a borrowed reference is returned. +/// - If the configuration is re-read, an owned copy is returned. +pub fn generic_content_config() -> Cow<'static, GenericContentConfig> { + fn gather() -> GenericContentConfig { + read_generic_content_config().unwrap_or_else(|e| { + error!("{e}"); + Default::default() + }) + } + if cache_content() { + Cow::Borrowed(GENERIC_CONTENT_CONFIG.get_or_init(gather)) + } else { + Cow::Owned(gather()) + } +} + /// Retrieves all generic pages, using the cache if it is enabled. /// /// This function returns a `Cow<'static, UrlToPageMap>` containing the generic pages. @@ -666,3 +738,31 @@ pub fn wiki_histories() -> Cow<'static, WikiHistories> { ) } } + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BasicSPA { + pub only_follow: bool, + pub no_indexing: bool, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SPAData { + BlogIndex, + HomePage, + NotFound, + #[serde(untagged)] + BasicSPA(BasicSPA), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildSPA { + pub slug: Cow<'static, str>, + pub page_title: Cow<'static, str>, + pub page_description: Option>, + pub trailing_slash: bool, + pub en_us_only: bool, + pub data: SPAData, +} diff --git a/crates/rari-doc/src/error.rs b/crates/rari-doc/src/error.rs index 1ca1131f..da0f14ff 100644 --- a/crates/rari-doc/src/error.rs +++ b/crates/rari-doc/src/error.rs @@ -28,6 +28,8 @@ pub enum DocError { NoCurriculumRoot, #[error("No generic content root set")] NoGenericContentRoot, + #[error("No generic content config found")] + NoGenericContentConfig, #[error("No H1 found")] NoH1, #[error(transparent)] diff --git a/crates/rari-doc/src/pages/build.rs b/crates/rari-doc/src/pages/build.rs index 8373e896..2b1e4ecb 100644 --- a/crates/rari-doc/src/pages/build.rs +++ b/crates/rari-doc/src/pages/build.rs @@ -341,11 +341,11 @@ fn build_generic_page(page: &GenericPage) -> Result { title: page.meta.title.clone(), toc, }, - page_title: concat_strs!( - page.meta.title.as_str(), - " | ", - page.meta.title_suffix.as_str() - ), + page_title: if let Some(suffix) = &page.meta.title_suffix { + concat_strs!(page.meta.title.as_str(), " | ", suffix) + } else { + page.meta.title.clone() + }, url: page.meta.url.clone(), id: page.meta.page.clone(), }))) diff --git a/crates/rari-doc/src/pages/types/generic.rs b/crates/rari-doc/src/pages/types/generic.rs index 8dc70a59..dd037b91 100644 --- a/crates/rari-doc/src/pages/types/generic.rs +++ b/crates/rari-doc/src/pages/types/generic.rs @@ -9,7 +9,7 @@ use rari_utils::concat_strs; use rari_utils::io::read_to_string; use serde::Deserialize; -use crate::cached_readers::generic_content_files; +use crate::cached_readers::{generic_content_config, generic_content_files, GenericPagesConfig}; use crate::error::DocError; use crate::pages::page::{Page, PageLike, PageReader}; use crate::utils::split_fm; @@ -27,7 +27,7 @@ pub struct GenericPageMeta { pub url: String, pub full_path: PathBuf, pub path: PathBuf, - pub title_suffix: String, + pub title_suffix: Option, pub page: String, } @@ -38,7 +38,7 @@ impl GenericPageMeta { path: PathBuf, locale: Locale, slug: String, - title_suffix: &str, + title_suffix: Option, page: String, ) -> Result { let url = concat_strs!( @@ -56,7 +56,7 @@ impl GenericPageMeta { url, path, full_path, - title_suffix: title_suffix.to_string(), + title_suffix, page, }) } @@ -70,30 +70,28 @@ impl PageReader for GenericPage { let path = path.into(); let root = generic_content_root().ok_or(DocError::NoGenericContentRoot)?; let without_root: &Path = path.strip_prefix(root)?; - let (slug_prefix, title_suffix, root) = if without_root.starts_with("plus/") { - (Some("plus/docs"), "MDN Plus", root.join("plus")) - } else if without_root.starts_with("community/") || without_root == Path::new("community") { - (None, "Contribute to MDN", root.join("community")) - } else if without_root.starts_with("observatory/") { - ( - Some("observatory/docs"), - "HTTP Observatory", - root.join("observatory"), - ) - } else { - return Err(DocError::PageNotFound( - path.to_string_lossy().to_string(), - crate::pages::page::PageCategory::GenericPage, - )); - }; - read_generic_page( - path, - locale.unwrap_or_default(), - slug_prefix, - title_suffix, - &root, - ) - .map(|g| Page::GenericPage(Arc::new(g))) + if let Some(section) = without_root.iter().next() { + let config = generic_content_config(); + let page_config = config.pages.get(section.to_string_lossy().as_ref()); + if let Some(GenericPagesConfig { + slug_prefix, + title_suffix, + }) = page_config + { + return read_generic_page( + &path, + locale.unwrap_or_default(), + slug_prefix.as_deref(), + title_suffix.as_deref(), + &root.join(section), + ) + .map(|g| Page::GenericPage(Arc::new(g))); + } + } + Err(DocError::PageNotFound( + path.to_string_lossy().to_string(), + crate::pages::page::PageCategory::GenericPage, + )) } } #[derive(Debug, Clone)] @@ -206,7 +204,7 @@ fn read_generic_page( path: impl Into, locale: Locale, slug_prefix: Option<&str>, - title_suffix: &str, + title_suffix: Option<&str>, root: &Path, ) -> Result { let full_path: PathBuf = path.into(); @@ -230,7 +228,7 @@ fn read_generic_page( path, locale, slug.to_string(), - title_suffix, + title_suffix.map(ToString::to_string), page.to_string(), )?, raw, diff --git a/crates/rari-doc/src/pages/types/spa.rs b/crates/rari-doc/src/pages/types/spa.rs index c1951a82..304c3cba 100644 --- a/crates/rari-doc/src/pages/types/spa.rs +++ b/crates/rari-doc/src/pages/types/spa.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; +use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use constcat::concat; -use phf::{phf_map, Map}; use rari_types::fm_types::{FeatureStatus, PageType}; use rari_types::globals::content_translated_root; use rari_types::locale::Locale; @@ -13,7 +13,7 @@ use rari_utils::concat_strs; use super::spa_homepage::{ featured_articles, featured_contributor, lastet_news, recent_contributions, }; -use crate::cached_readers::blog_files; +use crate::cached_readers::{blog_files, generic_content_config, BasicSPA, BuildSPA, SPAData}; use crate::error::DocError; use crate::helpers::title::page_title; use crate::pages::json::{ @@ -23,20 +23,6 @@ use crate::pages::json::{ use crate::pages::page::{Page, PageLike, PageReader}; use crate::pages::types::blog::BlogMeta; -#[derive(Debug, Clone, Copy)] -pub struct BasicSPA { - pub only_follow: bool, - pub no_indexing: bool, -} - -#[derive(Debug, Copy, Clone)] -pub enum SPAData { - BlogIndex, - HomePage, - NotFound, - BasicSPA(BasicSPA), -} - #[derive(Debug, Clone)] pub struct SPA { pub page_title: &'static str, @@ -62,13 +48,13 @@ impl SPA { None } else { Some(Page::SPA(Arc::new(SPA { - page_title: build_spa.page_title, - slug: build_spa.slug, + page_title: &build_spa.page_title, + slug: &build_spa.slug, url: concat_strs!( "/", locale.as_url_str(), "/", - build_spa.slug, + &build_spa.slug, if build_spa.trailing_slash && !build_spa.slug.is_empty() { "/" } else { @@ -79,7 +65,7 @@ impl SPA { page_type: PageType::SPA, data: build_spa.data, base_slug: Cow::Owned(concat_strs!("/", locale.as_url_str(), "/")), - page_description: build_spa.page_description, + page_description: build_spa.page_description.as_deref(), }))) } }) @@ -92,16 +78,16 @@ impl SPA { .unwrap_or_default() } - pub fn all() -> Vec<(&'static &'static str, Locale)> { + pub fn all() -> Vec<(String, Locale)> { BASIC_SPAS - .entries() + .iter() .flat_map(|(slug, build_spa)| { if build_spa.en_us_only || content_translated_root().is_none() { - vec![(slug, Locale::EnUs)] + vec![(slug.clone(), Locale::EnUs)] } else { Locale::for_generic_and_spas() .iter() - .map(|locale| (slug, *locale)) + .map(|locale| (slug.clone(), *locale)) .collect() } }) @@ -259,19 +245,9 @@ impl PageLike for SPA { } } -#[derive(Debug, Clone, Copy)] -pub struct BuildSPA { - pub slug: &'static str, - pub page_title: &'static str, - pub page_description: Option<&'static str>, - pub trailing_slash: bool, - pub en_us_only: bool, - pub data: SPAData, -} - const DEFAULT_BASIC_SPA: BuildSPA = BuildSPA { - slug: "", - page_title: "", + slug: Cow::Borrowed(""), + page_title: Cow::Borrowed(""), page_description: None, trailing_slash: false, en_us_only: false, @@ -284,104 +260,173 @@ const DEFAULT_BASIC_SPA: BuildSPA = BuildSPA { const MDN_PLUS_TITLE: &str = "MDN Plus"; const OBSERVATORY_TITLE_FULL: &str = "HTTP Observatory | MDN"; -const OBSERVATORY_DESCRIPTION: Option<&str> = -Some("Test your site’s HTTP headers, including CSP and HSTS, to find security problems and get actionable recommendations to make your website more secure. Test other websites to see how you compare."); +const OBSERVATORY_DESCRIPTION: &str = +"Test your site’s HTTP headers, including CSP and HSTS, to find security problems and get actionable recommendations to make your website more secure. Test other websites to see how you compare."; -static BASIC_SPAS: Map<&'static str, BuildSPA> = phf_map!( - "" => BuildSPA { - slug: "", - page_title: "MDN Web Docs", - page_description: None, - trailing_slash: true, - data: SPAData::HomePage, - ..DEFAULT_BASIC_SPA - }, - "404" => BuildSPA { - slug: "404", - page_title: "404", - page_description: None, - trailing_slash: false, - en_us_only: true, - data: SPAData::NotFound - }, - "blog" => BuildSPA { - slug: "blog", - page_title: "MDN Blog", - page_description: None, - trailing_slash: true, - en_us_only: true, - data: SPAData::BlogIndex - }, - "play" => BuildSPA { - slug: "play", - page_title: "Playground | MDN", - ..DEFAULT_BASIC_SPA - }, - "observatory" => BuildSPA { - slug: "observatory", - page_title: concat!("HTTP Header Security Test - ", OBSERVATORY_TITLE_FULL), - page_description: OBSERVATORY_DESCRIPTION, - ..DEFAULT_BASIC_SPA - }, - "observatory/analyze" => BuildSPA { - slug: "observatory/analyze", - page_title: concat!("Scan results - ", OBSERVATORY_TITLE_FULL), - page_description: OBSERVATORY_DESCRIPTION, - data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }), - ..DEFAULT_BASIC_SPA - }, - "search" => BuildSPA { - slug: "search", - page_title: "Search", - data: SPAData::BasicSPA(BasicSPA { only_follow: true, no_indexing: false }), - ..DEFAULT_BASIC_SPA - }, - "plus" => BuildSPA { - slug: "plus", - page_title: MDN_PLUS_TITLE, - ..DEFAULT_BASIC_SPA - }, - "plus/ai-help" => BuildSPA { - slug: "plus/ai-help", - page_title: concat!("AI Help | ", MDN_PLUS_TITLE), - ..DEFAULT_BASIC_SPA - }, - "plus/collections" => BuildSPA { - slug: "plus/collections", - page_title: concat!("Collections | ", MDN_PLUS_TITLE), - data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }), - ..DEFAULT_BASIC_SPA - }, - "plus/collections/frequently_viewed" => BuildSPA { - slug: "plus/collections/frequently_viewed", - page_title: concat!("Frequently viewed articles | ", MDN_PLUS_TITLE), - data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }), - ..DEFAULT_BASIC_SPA - }, - "plus/updates" => BuildSPA { - slug: "plus/updates", - page_title: concat!("Updates | ", MDN_PLUS_TITLE), - ..DEFAULT_BASIC_SPA - }, - "plus/settings" => BuildSPA { - slug: "plus/settings", - page_title: concat!("Settings | ", MDN_PLUS_TITLE), - data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }), - ..DEFAULT_BASIC_SPA - }, - "about" => BuildSPA { - slug: "about", - page_title: "About MDN", - ..DEFAULT_BASIC_SPA - }, - "advertising" => BuildSPA { - slug: "advertising", - page_title: "Advertise with us", - ..DEFAULT_BASIC_SPA - }, - "newsletter" => BuildSPA { - slug: "newsletter", - page_title: "Stay Informed with MDN", - ..DEFAULT_BASIC_SPA - }, -); +static BASIC_SPAS: LazyLock> = LazyLock::new(|| { + generic_content_config() + .spas + .clone() + .into_iter() + .chain( + [ + ( + "", + BuildSPA { + slug: Cow::Borrowed(""), + page_title: Cow::Borrowed("MDN Web Docs"), + page_description: None, + trailing_slash: true, + data: SPAData::HomePage, + ..DEFAULT_BASIC_SPA + }, + ), + ( + "404", + BuildSPA { + slug: Cow::Borrowed("404"), + page_title: Cow::Borrowed("404"), + page_description: None, + trailing_slash: false, + en_us_only: true, + data: SPAData::NotFound, + }, + ), + ( + "blog", + BuildSPA { + slug: Cow::Borrowed("blog"), + page_title: Cow::Borrowed("MDN Blog"), + page_description: None, + trailing_slash: true, + en_us_only: true, + data: SPAData::BlogIndex, + }, + ), + ( + "play", + BuildSPA { + slug: Cow::Borrowed("play"), + page_title: Cow::Borrowed("Playground | MDN"), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "observatory", + BuildSPA { + slug: Cow::Borrowed("observatory"), + page_title: Cow::Borrowed(concat!( + "HTTP Header Security Test - ", + OBSERVATORY_TITLE_FULL + )), + page_description: Some(Cow::Borrowed(OBSERVATORY_DESCRIPTION)), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "observatory/analyze", + BuildSPA { + slug: Cow::Borrowed("observatory/analyze"), + page_title: Cow::Borrowed(concat!( + "Scan results - ", + OBSERVATORY_TITLE_FULL + )), + page_description: Some(Cow::Borrowed(OBSERVATORY_DESCRIPTION)), + data: SPAData::BasicSPA(BasicSPA { + no_indexing: true, + only_follow: false, + }), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "search", + BuildSPA { + slug: Cow::Borrowed("search"), + page_title: Cow::Borrowed("Search"), + data: SPAData::BasicSPA(BasicSPA { + only_follow: true, + no_indexing: false, + }), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "plus/ai-help", + BuildSPA { + slug: Cow::Borrowed("plus/ai-help"), + page_title: Cow::Borrowed(concat!("AI Help | ", MDN_PLUS_TITLE)), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "plus/collections", + BuildSPA { + slug: Cow::Borrowed("plus/collections"), + page_title: Cow::Borrowed(concat!("Collections | ", MDN_PLUS_TITLE)), + data: SPAData::BasicSPA(BasicSPA { + no_indexing: true, + only_follow: false, + }), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "plus/collections/frequently_viewed", + BuildSPA { + slug: Cow::Borrowed("plus/collections/frequently_viewed"), + page_title: Cow::Borrowed(concat!( + "Frequently viewed articles | ", + MDN_PLUS_TITLE + )), + data: SPAData::BasicSPA(BasicSPA { + no_indexing: true, + only_follow: false, + }), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "plus/updates", + BuildSPA { + slug: Cow::Borrowed("plus/updates"), + page_title: Cow::Borrowed(concat!("Updates | ", MDN_PLUS_TITLE)), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "plus/settings", + BuildSPA { + slug: Cow::Borrowed("plus/settings"), + page_title: Cow::Borrowed(concat!("Settings | ", MDN_PLUS_TITLE)), + data: SPAData::BasicSPA(BasicSPA { + no_indexing: true, + only_follow: false, + }), + ..DEFAULT_BASIC_SPA + }, + ), + ( + "newsletter", + BuildSPA { + slug: Cow::Borrowed("newsletter"), + page_title: Cow::Borrowed("Stay Informed with MDN"), + ..DEFAULT_BASIC_SPA + }, + ), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v)), + ) + .collect() +}); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn print() { + println!("{}", serde_json::to_string(&*BASIC_SPAS).unwrap()) + } +}