//! Configuration, templating and discovery — the surface that decides whether this is a //! generator for one site or for anyone's. //! //! The theme running through these tests is that **the zero-config path has to work**. //! A directory of `.org` files with no `orgo.toml`, no templates and no knowledge of //! this tool must build into a real site; configuration is how you change the output, //! never how you make it work at all. use std::sync::atomic::{AtomicU32, Ordering}; use camino::Utf8PathBuf; use orgo::config::{Config, NavMode}; use orgo::site::{build_site, BuildOptions}; fn tmpdir(tag: &str) -> Utf8PathBuf { static N: AtomicU32 = AtomicU32::new(0); let n = N.fetch_add(1, Ordering::Relaxed); let base = Utf8PathBuf::from_path_buf(std::env::temp_dir()) .expect("utf-8 temp dir") .join(format!("orgo-cfg-{}-{tag}-{n}", std::process::id())); let _ = std::fs::remove_dir_all(&base); std::fs::create_dir_all(&base).unwrap(); base } /// A site with a root page, a second root page, and one nested page. fn write_site(src: &Utf8PathBuf) { std::fs::create_dir_all(src.join("blog")).unwrap(); std::fs::write(src.join("index.org"), "#+TITLE: Home\n\nWelcome.\n").unwrap(); std::fs::write(src.join("about.org"), "#+TITLE: About\n\nAbout.\n").unwrap(); std::fs::write( src.join("blog/post.org"), "#+TITLE: A Post\n#+DATE: 2024-05-01\n#+FILETAGS: :rust:web:\n\nBody.\n", ) .unwrap(); } fn build(src: &Utf8PathBuf, out: &Utf8PathBuf) -> orgo::site::SiteReport { build_site(src, out, &BuildOptions::default()).expect("build") } fn page(out: &Utf8PathBuf, rel: &str) -> String { std::fs::read_to_string(out.join(rel)).unwrap_or_else(|e| panic!("reading {rel}: {e}")) } // --------------------------------------------------------------------------- // Zero config // --------------------------------------------------------------------------- /// The headline promise: point it at a directory of org files and get a site. #[test] fn a_bare_directory_of_org_files_builds_with_no_config() { let root = tmpdir("bare"); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); write_site(&src); let out = root.join("out"); let report = build(&src, &out); assert_eq!(report.pages.len(), 3); let home = page(&out, "index.html"); assert!(home.contains(""), "a full page, not a fragment"); assert!(home.contains("Welcome."), "the content is there"); assert!( out.join("syntax.css").exists(), "the stylesheet the highlighter needs is emitted too" ); } /// A missing config is normal. A *malformed* one is not: someone who wrote a config /// meant it, and quietly building the default site would hide their typo behind /// plausible-looking output. #[test] fn a_malformed_config_is_an_error_but_a_missing_one_is_not() { let root = tmpdir("malformed"); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); write_site(&src); assert_eq!(Config::load(&src).unwrap(), Config::default()); std::fs::write(src.join("orgo.toml"), "[site\ntitle = broken").unwrap(); let err = Config::load(&src).expect_err("malformed config must fail"); assert!(format!("{err:#}").contains("orgo.toml"), "names the file: {err:#}"); } /// A misspelled key is a silent no-op in most config formats, which is exactly how /// someone spends an afternoon wondering why a setting does nothing. #[test] fn an_unknown_config_key_is_rejected() { let root = tmpdir("unknownkey"); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); std::fs::write(src.join("orgo.toml"), "[site]\ntittle = \"typo\"\n").unwrap(); let err = Config::load(&src).expect_err("unknown key must fail"); assert!( format!("{err:#}").contains("tittle"), "the error names the offending key: {err:#}" ); } // --------------------------------------------------------------------------- // Nav modes // --------------------------------------------------------------------------- fn nav_of(html: &str) -> String { html.split("").next()) .unwrap_or("") .to_string() } #[test] fn nav_modes_select_different_pages() { for (mode, expect_post, expect_about) in [ ("top-level", false, true), ("all", true, true), ("none", false, false), ] { let root = tmpdir(&format!("nav-{mode}")); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); write_site(&src); std::fs::write( src.join("orgo.toml"), format!("[nav]\nmode = \"{mode}\"\n"), ) .unwrap(); let out = root.join("out"); build(&src, &out); let nav = nav_of(&page(&out, "index.html")); assert_eq!( nav.contains("A Post"), expect_post, "mode {mode} nested page presence, nav was:\n{nav}" ); assert_eq!( nav.contains("About"), expect_about, "mode {mode} root page presence, nav was:\n{nav}" ); } } /// An explicit nav is a designed sequence, so configured order beats discovery order. #[test] fn explicit_nav_uses_the_configured_order() { let root = tmpdir("navexplicit"); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); write_site(&src); std::fs::write( src.join("orgo.toml"), "[nav]\nmode = \"explicit\"\npages = [\"blog/post.org\", \"index.org\"]\n", ) .unwrap(); let out = root.join("out"); build(&src, &out); let nav = nav_of(&page(&out, "index.html")); let post = nav.find("A Post").expect("post in nav"); let home = nav.find("Home").expect("home in nav"); assert!(post < home, "configured order wins:\n{nav}"); assert!(!nav.contains("About"), "unlisted pages stay out:\n{nav}"); } /// A nav entry naming a page that does not exist is a typo, and a silently shorter nav /// is a poor way to find out. #[test] fn explicit_nav_rejects_a_page_that_does_not_exist() { let root = tmpdir("navmissing"); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); write_site(&src); std::fs::write( src.join("orgo.toml"), "[nav]\nmode = \"explicit\"\npages = [\"nope.org\"]\n", ) .unwrap(); let err = build_site(&src, &root.join("out"), &BuildOptions::default()) .expect_err("missing nav page must fail"); assert!(format!("{err:#}").contains("nope.org"), "names it: {err:#}"); } /// `mode` and `pages` disagreeing means one of them is being ignored. #[test] fn contradictory_nav_settings_are_rejected() { let mut config = Config::default(); config.nav.pages = vec![Utf8PathBuf::from("index.org")]; assert!(config.validate().is_err(), "pages without explicit mode"); let mut config = Config::default(); config.nav.mode = NavMode::Explicit; assert!(config.validate().is_err(), "explicit mode without pages"); } // --------------------------------------------------------------------------- // Templates // --------------------------------------------------------------------------- /// The single biggest blocker to general use: without this every site built with this /// tool looks identical. #[test] fn a_user_template_replaces_the_built_in_layout() { let root = tmpdir("template"); let src = root.join("src"); std::fs::create_dir_all(src.join("templates")).unwrap(); write_site(&src); std::fs::write( src.join("templates/base.html"), "

{{ page.title }}

{{ body | safe }}", ) .unwrap(); let out = root.join("out"); build(&src, &out); let home = page(&out, "index.html"); assert!(home.contains("class=\"mine\""), "the user layout is used:\n{home}"); assert!(!home.contains("