Simplify docs preprocessing (#30947)

Ben Kunkle created

Closes #ISSUE

This was done as part of experimental work towards better validation of
our docs. The validation ended up being not worth it, however, I believe
this refactoring is

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/docs_preprocessor/Cargo.toml                  |   3 
crates/docs_preprocessor/src/docs_preprocessor.rs    |  94 ----------
crates/docs_preprocessor/src/main.rs                 | 124 ++++++++++++-
crates/docs_preprocessor/src/templates.rs            |  25 --
crates/docs_preprocessor/src/templates/action.rs     |  50 -----
crates/docs_preprocessor/src/templates/keybinding.rs |  41 ----
docs/README.md                                       |   2 
7 files changed, 110 insertions(+), 229 deletions(-)

Detailed changes

crates/docs_preprocessor/Cargo.toml 🔗

@@ -19,9 +19,6 @@ workspace-hack.workspace = true
 [lints]
 workspace = true
 
-[lib]
-path = "src/docs_preprocessor.rs"
-
 [[bin]]
 name = "docs_preprocessor"
 path = "src/main.rs"

crates/docs_preprocessor/src/docs_preprocessor.rs 🔗

@@ -1,94 +0,0 @@
-use anyhow::Result;
-use mdbook::book::{Book, BookItem};
-use mdbook::errors::Error;
-use mdbook::preprocess::{Preprocessor, PreprocessorContext as MdBookContext};
-use settings::KeymapFile;
-use std::sync::Arc;
-use util::asset_str;
-
-mod templates;
-
-use templates::{ActionTemplate, KeybindingTemplate, Template};
-
-pub struct PreprocessorContext {
-    macos_keymap: Arc<KeymapFile>,
-    linux_keymap: Arc<KeymapFile>,
-}
-
-impl PreprocessorContext {
-    pub fn new() -> Result<Self> {
-        let macos_keymap = Arc::new(load_keymap("keymaps/default-macos.json")?);
-        let linux_keymap = Arc::new(load_keymap("keymaps/default-linux.json")?);
-        Ok(Self {
-            macos_keymap,
-            linux_keymap,
-        })
-    }
-
-    pub fn find_binding(&self, os: &str, action: &str) -> Option<String> {
-        let keymap = match os {
-            "macos" => &self.macos_keymap,
-            "linux" => &self.linux_keymap,
-            _ => return None,
-        };
-
-        // Find the binding in reverse order, as the last binding takes precedence.
-        keymap.sections().rev().find_map(|section| {
-            section.bindings().rev().find_map(|(keystroke, a)| {
-                if a.to_string() == action {
-                    Some(keystroke.to_string())
-                } else {
-                    None
-                }
-            })
-        })
-    }
-}
-
-fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
-    let content = asset_str::<settings::SettingsAssets>(asset_path);
-    KeymapFile::parse(content.as_ref())
-}
-
-pub struct ZedDocsPreprocessor {
-    context: PreprocessorContext,
-    templates: Vec<Box<dyn Template>>,
-}
-
-impl ZedDocsPreprocessor {
-    pub fn new() -> Result<Self> {
-        let context = PreprocessorContext::new()?;
-        let templates: Vec<Box<dyn Template>> = vec![
-            Box::new(KeybindingTemplate::new()),
-            Box::new(ActionTemplate::new()),
-        ];
-        Ok(Self { context, templates })
-    }
-
-    fn process_content(&self, content: &str) -> String {
-        let mut processed = content.to_string();
-        for template in &self.templates {
-            processed = template.process(&self.context, &processed);
-        }
-        processed
-    }
-}
-
-impl Preprocessor for ZedDocsPreprocessor {
-    fn name(&self) -> &str {
-        "zed-docs-preprocessor"
-    }
-
-    fn run(&self, _ctx: &MdBookContext, mut book: Book) -> Result<Book, Error> {
-        book.for_each_mut(|item| {
-            if let BookItem::Chapter(chapter) = item {
-                chapter.content = self.process_content(&chapter.content);
-            }
-        });
-        Ok(book)
-    }
-
-    fn supports_renderer(&self, renderer: &str) -> bool {
-        renderer != "not-supported"
-    }
-}

crates/docs_preprocessor/src/main.rs 🔗

@@ -1,9 +1,21 @@
-use anyhow::{Context as _, Result};
+use anyhow::Result;
 use clap::{Arg, ArgMatches, Command};
-use docs_preprocessor::ZedDocsPreprocessor;
-use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
+use mdbook::BookItem;
+use mdbook::book::{Book, Chapter};
+use mdbook::preprocess::CmdPreprocessor;
+use regex::Regex;
+use settings::KeymapFile;
 use std::io::{self, Read};
 use std::process;
+use std::sync::LazyLock;
+
+static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
+    load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
+});
+
+static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
+    load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
+});
 
 pub fn make_app() -> Command {
     Command::new("zed-docs-preprocessor")
@@ -18,41 +30,123 @@ pub fn make_app() -> Command {
 fn main() -> Result<()> {
     let matches = make_app().get_matches();
 
-    let preprocessor =
-        ZedDocsPreprocessor::new().context("Failed to create ZedDocsPreprocessor")?;
-
     if let Some(sub_args) = matches.subcommand_matches("supports") {
-        handle_supports(&preprocessor, sub_args);
+        handle_supports(sub_args);
     } else {
-        handle_preprocessing(&preprocessor)?;
+        handle_preprocessing()?;
     }
 
     Ok(())
 }
 
-fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<()> {
+fn handle_preprocessing() -> Result<()> {
     let mut stdin = io::stdin();
     let mut input = String::new();
     stdin.read_to_string(&mut input)?;
 
-    let (ctx, book) = CmdPreprocessor::parse_input(input.as_bytes())?;
+    let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
 
-    let processed_book = pre.run(&ctx, book)?;
+    template_keybinding(&mut book);
+    template_action(&mut book);
 
-    serde_json::to_writer(io::stdout(), &processed_book)?;
+    serde_json::to_writer(io::stdout(), &book)?;
 
     Ok(())
 }
 
-fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
+fn handle_supports(sub_args: &ArgMatches) -> ! {
     let renderer = sub_args
         .get_one::<String>("renderer")
         .expect("Required argument");
-    let supported = pre.supports_renderer(renderer);
-
+    let supported = renderer != "not-supported";
     if supported {
         process::exit(0);
     } else {
         process::exit(1);
     }
 }
+
+fn template_keybinding(book: &mut Book) {
+    let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
+
+    for_each_chapter_mut(book, |chapter| {
+        chapter.content = regex
+            .replace_all(&chapter.content, |caps: &regex::Captures| {
+                let action = caps[1].trim();
+                let macos_binding = find_binding("macos", action).unwrap_or_default();
+                let linux_binding = find_binding("linux", action).unwrap_or_default();
+
+                if macos_binding.is_empty() && linux_binding.is_empty() {
+                    return "<div>No default binding</div>".to_string();
+                }
+
+                format!("<kbd class=\"keybinding\">{macos_binding}|{linux_binding}</kbd>")
+            })
+            .into_owned()
+    });
+}
+
+fn template_action(book: &mut Book) {
+    let regex = Regex::new(r"\{#action (.*?)\}").unwrap();
+
+    for_each_chapter_mut(book, |chapter| {
+        chapter.content = regex
+            .replace_all(&chapter.content, |caps: &regex::Captures| {
+                let name = caps[1].trim();
+
+                let formatted_name = name
+                    .chars()
+                    .enumerate()
+                    .map(|(i, c)| {
+                        if i > 0 && c.is_uppercase() {
+                            format!(" {}", c.to_lowercase())
+                        } else {
+                            c.to_string()
+                        }
+                    })
+                    .collect::<String>()
+                    .trim()
+                    .to_string()
+                    .replace("::", ":");
+
+                format!("<code class=\"hljs\">{}</code>", formatted_name)
+            })
+            .into_owned()
+    });
+}
+
+fn find_binding(os: &str, action: &str) -> Option<String> {
+    let keymap = match os {
+        "macos" => &KEYMAP_MACOS,
+        "linux" => &KEYMAP_LINUX,
+        _ => unreachable!("Not a valid OS: {}", os),
+    };
+
+    // Find the binding in reverse order, as the last binding takes precedence.
+    keymap.sections().rev().find_map(|section| {
+        section.bindings().rev().find_map(|(keystroke, a)| {
+            if a.to_string() == action {
+                Some(keystroke.to_string())
+            } else {
+                None
+            }
+        })
+    })
+}
+
+fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
+    let content = util::asset_str::<settings::SettingsAssets>(asset_path);
+    KeymapFile::parse(content.as_ref())
+}
+
+fn for_each_chapter_mut<F>(book: &mut Book, mut func: F)
+where
+    F: FnMut(&mut Chapter),
+{
+    book.for_each_mut(|item| {
+        let BookItem::Chapter(chapter) = item else {
+            return;
+        };
+        func(chapter);
+    });
+}

crates/docs_preprocessor/src/templates.rs 🔗

@@ -1,25 +0,0 @@
-use crate::PreprocessorContext;
-use regex::Regex;
-use std::collections::HashMap;
-
-mod action;
-mod keybinding;
-
-pub use action::*;
-pub use keybinding::*;
-
-pub trait Template {
-    fn key(&self) -> &'static str;
-    fn regex(&self) -> Regex;
-    fn parse_args(&self, args: &str) -> HashMap<String, String>;
-    fn render(&self, context: &PreprocessorContext, args: &HashMap<String, String>) -> String;
-
-    fn process(&self, context: &PreprocessorContext, content: &str) -> String {
-        self.regex()
-            .replace_all(content, |caps: &regex::Captures| {
-                let args = self.parse_args(&caps[1]);
-                self.render(context, &args)
-            })
-            .into_owned()
-    }
-}

crates/docs_preprocessor/src/templates/action.rs 🔗

@@ -1,50 +0,0 @@
-use crate::PreprocessorContext;
-use regex::Regex;
-use std::collections::HashMap;
-
-use super::Template;
-
-pub struct ActionTemplate;
-
-impl ActionTemplate {
-    pub fn new() -> Self {
-        ActionTemplate
-    }
-}
-
-impl Template for ActionTemplate {
-    fn key(&self) -> &'static str {
-        "action"
-    }
-
-    fn regex(&self) -> Regex {
-        Regex::new(&format!(r"\{{#{}(.*?)\}}", self.key())).unwrap()
-    }
-
-    fn parse_args(&self, args: &str) -> HashMap<String, String> {
-        let mut map = HashMap::new();
-        map.insert("name".to_string(), args.trim().to_string());
-        map
-    }
-
-    fn render(&self, _context: &PreprocessorContext, args: &HashMap<String, String>) -> String {
-        let name = args.get("name").map(String::as_str).unwrap_or_default();
-
-        let formatted_name = name
-            .chars()
-            .enumerate()
-            .map(|(i, c)| {
-                if i > 0 && c.is_uppercase() {
-                    format!(" {}", c.to_lowercase())
-                } else {
-                    c.to_string()
-                }
-            })
-            .collect::<String>()
-            .trim()
-            .to_string()
-            .replace("::", ":");
-
-        format!("<code class=\"hljs\">{}</code>", formatted_name)
-    }
-}

crates/docs_preprocessor/src/templates/keybinding.rs 🔗

@@ -1,41 +0,0 @@
-use crate::PreprocessorContext;
-use regex::Regex;
-use std::collections::HashMap;
-
-use super::Template;
-
-pub struct KeybindingTemplate;
-
-impl KeybindingTemplate {
-    pub fn new() -> Self {
-        KeybindingTemplate
-    }
-}
-
-impl Template for KeybindingTemplate {
-    fn key(&self) -> &'static str {
-        "kb"
-    }
-
-    fn regex(&self) -> Regex {
-        Regex::new(&format!(r"\{{#{}(.*?)\}}", self.key())).unwrap()
-    }
-
-    fn parse_args(&self, args: &str) -> HashMap<String, String> {
-        let mut map = HashMap::new();
-        map.insert("action".to_string(), args.trim().to_string());
-        map
-    }
-
-    fn render(&self, context: &PreprocessorContext, args: &HashMap<String, String>) -> String {
-        let action = args.get("action").map(String::as_str).unwrap_or("");
-        let macos_binding = context.find_binding("macos", action).unwrap_or_default();
-        let linux_binding = context.find_binding("linux", action).unwrap_or_default();
-
-        if macos_binding.is_empty() && linux_binding.is_empty() {
-            return "<div>No default binding</div>".to_string();
-        }
-
-        format!("<kbd class=\"keybinding\">{macos_binding}|{linux_binding}</kbd>")
-    }
-}

docs/README.md 🔗

@@ -62,7 +62,7 @@ This will render a human-readable version of the action name, e.g., "zed: open s
 
 ### Creating New Templates
 
-New templates can be created by implementing the `Template` trait for your desired template in the `docs_preprocessor` crate.
+Templates are just functions that modify the source of the docs pages (usually with a regex match & replace). You can see how the actions and keybindings are templated in `crates/docs_preprocessor/src/main.rs` for reference on how to create new templates.
 
 ### References