Simplify docs preprocessing (#30947)

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 ...
This commit is contained in:
Ben Kunkle 2025-05-19 07:16:14 -05:00 committed by GitHub
parent 57424e4743
commit b057b4697f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 229 deletions

View file

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

View file

@ -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"
}
}

View file

@ -1,9 +1,21 @@
use anyhow::{Context as _, Result}; use anyhow::Result;
use clap::{Arg, ArgMatches, Command}; use clap::{Arg, ArgMatches, Command};
use docs_preprocessor::ZedDocsPreprocessor; use mdbook::BookItem;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; use mdbook::book::{Book, Chapter};
use mdbook::preprocess::CmdPreprocessor;
use regex::Regex;
use settings::KeymapFile;
use std::io::{self, Read}; use std::io::{self, Read};
use std::process; 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 { pub fn make_app() -> Command {
Command::new("zed-docs-preprocessor") Command::new("zed-docs-preprocessor")
@ -18,41 +30,123 @@ pub fn make_app() -> Command {
fn main() -> Result<()> { fn main() -> Result<()> {
let matches = make_app().get_matches(); let matches = make_app().get_matches();
let preprocessor =
ZedDocsPreprocessor::new().context("Failed to create ZedDocsPreprocessor")?;
if let Some(sub_args) = matches.subcommand_matches("supports") { if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(&preprocessor, sub_args); handle_supports(sub_args);
} else { } else {
handle_preprocessing(&preprocessor)?; handle_preprocessing()?;
} }
Ok(()) Ok(())
} }
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<()> { fn handle_preprocessing() -> Result<()> {
let mut stdin = io::stdin(); let mut stdin = io::stdin();
let mut input = String::new(); let mut input = String::new();
stdin.read_to_string(&mut input)?; 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(()) Ok(())
} }
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! { fn handle_supports(sub_args: &ArgMatches) -> ! {
let renderer = sub_args let renderer = sub_args
.get_one::<String>("renderer") .get_one::<String>("renderer")
.expect("Required argument"); .expect("Required argument");
let supported = pre.supports_renderer(renderer); let supported = renderer != "not-supported";
if supported { if supported {
process::exit(0); process::exit(0);
} else { } else {
process::exit(1); 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);
});
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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>")
}
}

View file

@ -62,7 +62,7 @@ This will render a human-readable version of the action name, e.g., "zed: open s
### Creating New Templates ### 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 ### References