diff --git a/components/config/src/lib.rs b/components/config/src/lib.rs index 3f11428..00dc1bc 100644 --- a/components/config/src/lib.rs +++ b/components/config/src/lib.rs @@ -24,7 +24,12 @@ mod theme; use theme::Theme; +// We want a default base url for tests +static DEFAULT_BASE_URL: &'static str = "http://a-website.com"; + + #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(default)] pub struct Config { /// Base URL of the site, the only required config argument pub base_url: String, @@ -33,48 +38,47 @@ pub struct Config { pub theme: Option, /// Title of the site. Defaults to None pub title: Option, - /// Whether to highlight all code blocks found in markdown files. Defaults to false - pub highlight_code: Option, - /// Which themes to use for code highlighting. See Readme for supported themes - pub highlight_theme: Option, /// Description of the site pub description: Option, + /// The language used in the site. Defaults to "en" - pub default_language: Option, + pub default_language: String, + /// Languages list and translated strings + pub translations: HashMap, + + /// Whether to highlight all code blocks found in markdown files. Defaults to false + pub highlight_code: bool, + /// Which themes to use for code highlighting. See Readme for supported themes + /// Defaults to "base16-ocean-dark" + pub highlight_theme: String, + /// Whether to generate RSS. Defaults to false - pub generate_rss: Option, - /// The number of articles to include in the RSS feed. Defaults to unlimited - pub rss_limit: Option, + pub generate_rss: bool, + /// The number of articles to include in the RSS feed. Defaults to 10_000 + pub rss_limit: usize, /// Whether to generate tags and individual tag pages if some pages have them. Defaults to true - pub generate_tags_pages: Option, + pub generate_tags_pages: bool, /// Whether to generate categories and individual tag categories if some pages have them. Defaults to true - pub generate_categories_pages: Option, + pub generate_categories_pages: bool, + /// Whether to compile the `sass` directory and output the css files into the static folder - pub compile_sass: Option, + pub compile_sass: bool, + /// Whether to build the search index for the content + pub build_search_index: bool, /// A list of file glob patterns to ignore when processing the content folder. Defaults to none. /// Had to remove the PartialEq derive because GlobSet does not implement it. No impact /// because it's unused anyway (who wants to sort Configs?). - pub ignored_content: Option>, + pub ignored_content: Vec, #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed - pub ignored_content_globber: Option, - - /// Languages list and translated strings - pub translations: Option>, + pub ignored_content_globset: Option, /// All user params set in [extra] in the config - pub extra: Option>, + pub extra: HashMap, /// Set automatically when instantiating the config. Used for cachebusting pub build_timestamp: Option, } -macro_rules! set_default { - ($key: expr, $default: expr) => { - if $key.is_none() { - $key = Some($default); - } - } -} impl Config { /// Parses a string containing TOML to our Config struct @@ -85,45 +89,33 @@ impl Config { Err(e) => bail!(e) }; - set_default!(config.default_language, "en".to_string()); - set_default!(config.highlight_code, false); - set_default!(config.generate_rss, false); - set_default!(config.rss_limit, 20); - set_default!(config.generate_tags_pages, false); - set_default!(config.generate_categories_pages, false); - set_default!(config.compile_sass, false); - set_default!(config.ignored_content, Vec::new()); - set_default!(config.translations, HashMap::new()); - set_default!(config.extra, HashMap::new()); - - match config.highlight_theme { - Some(ref t) => { - if !THEME_SET.themes.contains_key(t) { - bail!("Theme {} not available", t) - } - } - None => config.highlight_theme = Some("base16-ocean-dark".to_string()) - }; + if config.base_url.is_empty() || config.base_url == DEFAULT_BASE_URL { + bail!("A base URL is required in config.toml with key `base_url`"); + } + + if !THEME_SET.themes.contains_key(&config.highlight_theme) { + bail!("Highlight theme {} not available", config.highlight_theme) + } config.build_timestamp = Some(Utc::now().timestamp()); - // Convert the file glob strings into a compiled glob set matcher. We want to do this once, - // at program initialization, rather than for every page, for example. We arrange for the - // globset matcher to always exist (even though it has to be an inside an Option at the - // moment because of the TOML serializer); if the glob set is empty the `is_match` function - // of the globber always returns false. - let mut glob_set_builder = GlobSetBuilder::new(); - if let Some(ref v) = config.ignored_content { - for pat in v { + if !config.ignored_content.is_empty() { + // Convert the file glob strings into a compiled glob set matcher. We want to do this once, + // at program initialization, rather than for every page, for example. We arrange for the + // globset matcher to always exist (even though it has to be an inside an Option at the + // moment because of the TOML serializer); if the glob set is empty the `is_match` function + // of the globber always returns false. + let mut glob_set_builder = GlobSetBuilder::new(); + for pat in &config.ignored_content { let glob = match Glob::new(pat) { Ok(g) => g, Err(e) => bail!("Invalid ignored_content glob pattern: {}, error = {}", pat, e) }; glob_set_builder.add(glob); } + config.ignored_content_globset = Some(glob_set_builder.build().expect("Bad ignored_content in config file.")); } - config.ignored_content_globber = Some(glob_set_builder.build().expect("Bad ignored_content in config file.")); Ok(config) } @@ -161,19 +153,17 @@ impl Config { /// Merges the extra data from the theme with the config extra data fn add_theme_extra(&mut self, theme: &Theme) -> Result<()> { - if let Some(ref mut config_extra) = self.extra { - // 3 pass merging - // 1. save config to preserve user - let original = config_extra.clone(); - // 2. inject theme extra values - for (key, val) in &theme.extra { - config_extra.entry(key.to_string()).or_insert_with(|| val.clone()); - } + // 3 pass merging + // 1. save config to preserve user + let original = self.extra.clone(); + // 2. inject theme extra values + for (key, val) in &theme.extra { + self.extra.entry(key.to_string()).or_insert_with(|| val.clone()); + } - // 3. overwrite with original config - for (key, val) in &original { - config_extra.entry(key.to_string()).or_insert_with(|| val.clone()); - } + // 3. overwrite with original config + for (key, val) in &original { + self.extra.entry(key.to_string()).or_insert_with(|| val.clone()); } Ok(()) @@ -187,27 +177,26 @@ impl Config { } } -/// Exists only for testing purposes -#[doc(hidden)] impl Default for Config { fn default() -> Config { Config { - title: Some("".to_string()), - theme: None, - base_url: "http://a-website.com/".to_string(), - highlight_code: Some(true), - highlight_theme: Some("base16-ocean-dark".to_string()), + base_url: DEFAULT_BASE_URL.to_string(), + title: None, description: None, - default_language: Some("en".to_string()), - generate_rss: Some(false), - rss_limit: Some(10_000), - generate_tags_pages: Some(true), - generate_categories_pages: Some(true), - compile_sass: Some(false), - ignored_content: Some(Vec::new()), - ignored_content_globber: Some(GlobSetBuilder::new().build().unwrap()), - translations: None, - extra: None, + theme: None, + highlight_code: true, + highlight_theme: "base16-ocean-dark".to_string(), + default_language: "en".to_string(), + generate_rss: false, + rss_limit: 10_000, + generate_tags_pages: true, + generate_categories_pages: true, + compile_sass: false, + build_search_index: false, + ignored_content: Vec::new(), + ignored_content_globset: None, + translations: HashMap::new(), + extra: HashMap::new(), build_timestamp: Some(1), } } @@ -277,7 +266,7 @@ hello = "world" let config = Config::parse(config); assert!(config.is_ok()); - assert_eq!(config.unwrap().extra.unwrap().get("hello").unwrap().as_str().unwrap(), "world"); + assert_eq!(config.unwrap().extra.get("hello").unwrap().as_str().unwrap(), "world"); } #[test] @@ -333,7 +322,7 @@ a_value = 10 "#; let theme = Theme::parse(theme_str).unwrap(); assert!(config.add_theme_extra(&theme).is_ok()); - let extra = config.extra.unwrap(); + let extra = config.extra; assert_eq!(extra["hello"].as_str().unwrap(), "world".to_string()); assert_eq!(extra["a_value"].as_integer().unwrap(), 10); } @@ -355,26 +344,26 @@ title = "A title" let config = Config::parse(config); assert!(config.is_ok()); - let translations = config.unwrap().translations.unwrap(); + let translations = config.unwrap().translations; assert_eq!(translations["fr"]["title"].as_str().unwrap(), "Un titre"); assert_eq!(translations["en"]["title"].as_str().unwrap(), "A title"); } #[test] - fn missing_ignored_content_results_in_empty_vector_and_empty_globber() { + fn missing_ignored_content_results_in_empty_vector_and_empty_globset() { let config_str = r#" title = "My site" base_url = "example.com" "#; let config = Config::parse(config_str).unwrap(); - let v = config.ignored_content.unwrap(); + let v = config.ignored_content; assert_eq!(v.len(), 0); - assert!(config.ignored_content_globber.unwrap().is_empty()); + assert!(config.ignored_content_globset.is_none()); } #[test] - fn empty_ignored_content_results_in_empty_vector_and_empty_globber() { + fn empty_ignored_content_results_in_empty_vector_and_empty_globset() { let config_str = r#" title = "My site" base_url = "example.com" @@ -382,12 +371,12 @@ ignored_content = [] "#; let config = Config::parse(config_str).unwrap(); - assert_eq!(config.ignored_content.unwrap().len(), 0); - assert!(config.ignored_content_globber.unwrap().is_empty()); + assert_eq!(config.ignored_content.len(), 0); + assert!(config.ignored_content_globset.is_none()); } #[test] - fn non_empty_ignored_content_results_in_vector_of_patterns_and_configured_globber() { + fn non_empty_ignored_content_results_in_vector_of_patterns_and_configured_globset() { let config_str = r#" title = "My site" base_url = "example.com" @@ -395,10 +384,10 @@ ignored_content = ["*.{graphml,iso}", "*.py?"] "#; let config = Config::parse(config_str).unwrap(); - let v = config.ignored_content.unwrap(); + let v = config.ignored_content; assert_eq!(v, vec!["*.{graphml,iso}", "*.py?"]); - let g = config.ignored_content_globber.unwrap(); + let g = config.ignored_content_globset.unwrap(); assert_eq!(g.len(), 2); assert!(g.is_match("foo.graphml")); assert!(g.is_match("foo.iso")); diff --git a/components/content/src/page.rs b/components/content/src/page.rs index 220a019..c02680d 100644 --- a/components/content/src/page.rs +++ b/components/content/src/page.rs @@ -130,23 +130,27 @@ impl Page { let mut page = Page::parse(path, &content, config)?; if page.file.name == "index" { - // `find_related_assets` only scans the immediate directory (it is not recursive) so our - // filtering only needs to work against the file_name component, not the full suffix. If - // `find_related_assets` was changed to also return files in subdirectories, we could - // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter - // against the remaining path. Note that the current behaviour effectively means that - // the `ignored_content` setting in the config file is limited to single-file glob - // patterns (no "**" patterns). - let globber = config.ignored_content_globber.as_ref().unwrap(); let parent_dir = path.parent().unwrap(); - page.assets = find_related_assets(parent_dir).into_iter() - .filter(|path| - match path.file_name() { - None => true, - Some(file) => !globber.is_match(file) - } - ).collect(); - + let assets = find_related_assets(parent_dir); + + if let Some(ref globset) = config.ignored_content_globset { + // `find_related_assets` only scans the immediate directory (it is not recursive) so our + // filtering only needs to work against the file_name component, not the full suffix. If + // `find_related_assets` was changed to also return files in subdirectories, we could + // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter + // against the remaining path. Note that the current behaviour effectively means that + // the `ignored_content` setting in the config file is limited to single-file glob + // patterns (no "**" patterns). + page.assets = assets.into_iter() + .filter(|path| + match path.file_name() { + None => true, + Some(file) => !globset.is_match(file) + } + ).collect(); + } else { + page.assets = assets; + } } else { page.assets = vec![]; } @@ -160,8 +164,8 @@ impl Page { pub fn render_markdown(&mut self, permalinks: &HashMap, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { let context = Context::new( tera, - config.highlight_code.unwrap(), - config.highlight_theme.clone().unwrap(), + config.highlight_code, + config.highlight_theme.clone(), &self.permalink, permalinks, anchor_insert @@ -450,7 +454,7 @@ Hello world let mut gsb = GlobSetBuilder::new(); gsb.add(Glob::new("*.{js,png}").unwrap()); let mut config = Config::default(); - config.ignored_content_globber = Some(gsb.build().unwrap()); + config.ignored_content_globset = Some(gsb.build().unwrap()); let res = Page::from_file( nested_path.join("index.md").as_path(), diff --git a/components/content/src/section.rs b/components/content/src/section.rs index df94d32..8ce6dfe 100644 --- a/components/content/src/section.rs +++ b/components/content/src/section.rs @@ -100,8 +100,8 @@ impl Section { pub fn render_markdown(&mut self, permalinks: &HashMap, tera: &Tera, config: &Config) -> Result<()> { let context = Context::new( tera, - config.highlight_code.unwrap(), - config.highlight_theme.clone().unwrap(), + config.highlight_code, + config.highlight_theme.clone(), &self.permalink, permalinks, self.meta.insert_anchor_links.unwrap() diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 1e776b9..661adb8 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -376,8 +376,8 @@ impl Site { /// Find all the tags and categories if it's asked in the config pub fn populate_tags_and_categories(&mut self) { - let generate_tags_pages = self.config.generate_tags_pages.unwrap(); - let generate_categories_pages = self.config.generate_categories_pages.unwrap(); + let generate_tags_pages = self.config.generate_tags_pages; + let generate_categories_pages = self.config.generate_categories_pages; if !generate_tags_pages && !generate_categories_pages { return; } @@ -505,7 +505,7 @@ impl Site { self.render_sections()?; self.render_orphan_pages()?; self.render_sitemap()?; - if self.config.generate_rss.unwrap() { + if self.config.generate_rss { self.render_rss_feed()?; } self.render_robots()?; @@ -521,7 +521,7 @@ impl Site { } } - if self.config.compile_sass.unwrap() { + if self.config.compile_sass { self.compile_sass(&self.base_path)?; } @@ -703,7 +703,7 @@ impl Site { let (sorted_pages, _) = sort_pages(pages, SortBy::Date); context.add("last_build_date", &sorted_pages[0].meta.date.clone().map(|d| d.to_string())); // limit to the last n elements) - context.add("pages", &sorted_pages.iter().take(self.config.rss_limit.unwrap()).collect::>()); + context.add("pages", &sorted_pages.iter().take(self.config.rss_limit).collect::>()); context.add("config", &self.config); let rss_feed_url = if self.config.base_url.ends_with('/') { diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index af82538..efef856 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -193,7 +193,7 @@ fn can_build_site_with_categories() { let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); path.push("test_site"); let mut site = Site::new(&path, "config.toml").unwrap(); - site.config.generate_categories_pages = Some(true); + site.config.generate_categories_pages = true; site.load().unwrap(); for (i, page) in site.pages.values_mut().enumerate() { @@ -247,7 +247,7 @@ fn can_build_site_with_tags() { let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); path.push("test_site"); let mut site = Site::new(&path, "config.toml").unwrap(); - site.config.generate_tags_pages = Some(true); + site.config.generate_tags_pages = true; site.load().unwrap(); for (i, page) in site.pages.values_mut().enumerate() { diff --git a/components/templates/src/global_fns.rs b/components/templates/src/global_fns.rs index dd149c7..468c910 100644 --- a/components/templates/src/global_fns.rs +++ b/components/templates/src/global_fns.rs @@ -23,8 +23,8 @@ macro_rules! required_string_arg { pub fn make_trans(config: Config) -> GlobalFn { - let translations_config = config.translations.unwrap(); - let default_lang = to_value(config.default_language.unwrap()).unwrap(); + let translations_config = config.translations; + let default_lang = to_value(config.default_language).unwrap(); Box::new(move |args| -> Result { let key = required_string_arg!(args.get("key"), "`trans` requires a `key` argument."); diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 5afd75e..8aae24a 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -19,6 +19,9 @@ compile_sass = %COMPILE_SASS% # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Gutenberg highlight_code = %HIGHLIGHT% +# Whether to build a search index to be used later on by a JavaScript library +build_search_index = %SEARCH% + [extra] # Put all your custom variables here "#; @@ -37,11 +40,13 @@ pub fn create_new_project(name: &str) -> Result<()> { let base_url = ask_url("> What is the URL of your site?", "https://example.com")?; let compile_sass = ask_bool("> Do you want to enable Sass compilation?", true)?; let highlight = ask_bool("> Do you want to enable syntax highlighting?", false)?; + let search = ask_bool("> Do you want to build a search index of the content?", false)?; let config = CONFIG .trim_left() .replace("%BASE_URL%", &base_url) .replace("%COMPILE_SASS%", &format!("{}", compile_sass)) + .replace("%SEARCH%", &format!("{}", search)) .replace("%HIGHLIGHT%", &format!("{}", highlight)); create_file(&path.join("config.toml"), &config)?; @@ -53,6 +58,7 @@ pub fn create_new_project(name: &str) -> Result<()> { if compile_sass { create_dir(path.join("sass"))?; } + // TODO: if search == true, copy a lunr js file embedded in gutenberg println!(); console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap())); diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index dac394a..edf6806 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -163,7 +163,7 @@ pub fn serve(interface: &str, port: &str, output_dir: &str, base_url: &str, conf if watching_static { watchers.push("static"); } - if site.config.compile_sass.unwrap() { + if site.config.compile_sass { watchers.push("sass"); }