//! Auto-generate markdown documentation. use std::collections::HashMap; use std::path::PathBuf; use cove_input::KeyBinding; pub use cove_macro::Document; use serde::Serialize; const MARKDOWN_INTRODUCTION: &str = r#"# Config file format Cove's config file uses the [TOML](https://toml.io/) format. ## Key binding format Key bindings are specified as strings or lists of strings. Each string specifies a main key and zero or more modifier keys. The modifier keys (if any) are listed first, followed by the main key. They are separated by the `+` character and **no** whitespace. Examples of key bindings: - `"ctrl+c"` - `"X"` (not `"shift+x"`) - `"space"` or `" "` (both space bar) - `["g", "home"]` - `["K", "ctrl+up"]` - `["f1", "?"]` - `"ctrl+alt+f3"` - `["enter", "any+enter"]` (matches `enter` regardless of modifiers) Available main keys: - Any single character that can be typed - `esc`, `enter`, `space`, `tab`, `backtab` - `backspace`, `delete`, `insert` - `left`, `right`, `up`, `down` - `home`, `end`, `pageup`, `pagedown` - `f1`, `f2`, ... Available modifiers: - `shift` (must not be used with single characters) - `ctrl` - `alt` - `any` (matches as long as at least one modifier is pressed) ## Config options "#; pub fn toml_value_as_markdown(value: &T) -> String { let mut result = String::new(); value .serialize(toml::ser::ValueSerializer::new(&mut result)) .expect("not a valid toml value"); format!("`{result}`") } #[derive(Clone, Default)] pub struct ValueInfo { pub required: Option, pub r#type: Option, pub values: Option>, pub default: Option, } impl ValueInfo { fn as_markdown(&self) -> String { let mut lines = vec![]; if let Some(required) = self.required { let yesno = if required { "yes" } else { "no" }; lines.push(format!("**Required:** {yesno}")); } if let Some(r#type) = &self.r#type { lines.push(format!("**Type:** {type}")); } if let Some(values) = &self.values { let values = values.join(", "); lines.push(format!("**Values:** {values}")); } if let Some(default) = &self.default { lines.push(format!("**Default:** {default}")); } lines.join(" \n") } } #[derive(Clone, Default)] pub struct StructInfo { pub fields: HashMap>, } #[derive(Clone, Default)] pub struct WrapInfo { pub inner: Option>, pub metavar: Option, } #[derive(Clone, Default)] pub struct Doc { pub description: Option, pub value_info: ValueInfo, pub struct_info: StructInfo, pub wrap_info: WrapInfo, } struct Entry { path: String, description: String, value_info: ValueInfo, } impl Entry { fn new(description: String, value_info: ValueInfo) -> Self { Self { path: String::new(), description, value_info, } } fn with_parent(mut self, segment: String) -> Self { if self.path.is_empty() { self.path = segment; } else { self.path = format!("{segment}.{}", self.path); } self } } impl Doc { fn entries(&self) -> Vec { let mut entries = vec![]; if let Some(description) = &self.description { entries.push(Entry::new(description.clone(), self.value_info.clone())); } for (segment, field) in &self.struct_info.fields { entries.extend( field .entries() .into_iter() .map(|entry| entry.with_parent(segment.clone())), ); } if let Some(inner) = &self.wrap_info.inner { let segment = match &self.wrap_info.metavar { Some(metavar) => format!("<{metavar}>"), None => "<...>".to_string(), }; entries.extend( inner .entries() .into_iter() .map(|entry| entry.with_parent(segment.clone())), ); } entries } pub fn as_markdown(&self) -> String { // Print entries in alphabetical order to make generated documentation // format more stable. let mut entries = self.entries(); entries.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut result = String::new(); result.push_str(MARKDOWN_INTRODUCTION); for entry in entries { result.push_str(&format!("\n### `{}`\n", entry.path)); let value_info = entry.value_info.as_markdown(); if !value_info.is_empty() { result.push_str(&format!("\n{value_info}\n")); } if !entry.description.is_empty() { result.push_str(&format!("\n{}\n", entry.description)); } } result } } pub trait Document { fn doc() -> Doc; } impl Document for String { fn doc() -> Doc { let mut doc = Doc::default(); doc.value_info.required = Some(true); doc.value_info.r#type = Some("string".to_string()); doc } } impl Document for bool { fn doc() -> Doc { let mut doc = Doc::default(); doc.value_info.required = Some(true); doc.value_info.r#type = Some("boolean".to_string()); doc } } impl Document for PathBuf { fn doc() -> Doc { let mut doc = Doc::default(); doc.value_info.required = Some(true); doc.value_info.r#type = Some("path".to_string()); doc } } impl Document for Option { fn doc() -> Doc { let mut doc = I::doc(); assert_eq!(doc.value_info.required, Some(true)); doc.value_info.required = Some(false); doc } } impl Document for HashMap { fn doc() -> Doc { let mut doc = Doc::default(); doc.wrap_info.inner = Some(Box::new(I::doc())); doc } } impl Document for KeyBinding { fn doc() -> Doc { let mut doc = Doc::default(); doc.value_info.required = Some(true); doc.value_info.r#type = Some("key binding".to_string()); doc } }