use anyhow::Result;use clap::{Arg, ArgMatches, Command};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") .about("Preprocesses Zed Docs content to provide rich action & keybinding support and more") .subcommand( Command::new("supports") .arg(Arg::new("renderer").required(true)) .about("Check whether a renderer is supported by this preprocessor"), )} fn main() -> Result<()> { let matches = make_app().get_matches(); if let Some(sub_args) = matches.subcommand_matches("supports") { handle_supports(sub_args); } else { handle_preprocessing()?; } Ok(())} fn handle_preprocessing() -> Result<()> { let mut stdin = io::stdin(); let mut input = String::new(); stdin.read_to_string(&mut input)?; let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?; template_keybinding(&mut book); template_action(&mut book); serde_json::to_writer(io::stdout(), &book)?; Ok(())} fn handle_supports(sub_args: &ArgMatches) -> ! { let renderer = sub_args .get_one::<String>("renderer") .expect("Required argument"); 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: ®ex::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: ®ex::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); });}
use anyhow::Result;
use clap::{Arg, ArgMatches, Command};
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")
.about("Preprocesses Zed Docs content to provide rich action & keybinding support and more")
.subcommand(
Command::new("supports")
.arg(Arg::new("renderer").required(true))
.about("Check whether a renderer is supported by this preprocessor"),
)
}
fn main() -> Result<()> {
let matches = make_app().get_matches();
if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(sub_args);
} else {
handle_preprocessing()?;
}
Ok(())
}
fn handle_preprocessing() -> Result<()> {
let mut stdin = io::stdin();
let mut input = String::new();
stdin.read_to_string(&mut input)?;
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
template_keybinding(&mut book);
template_action(&mut book);
serde_json::to_writer(io::stdout(), &book)?;
Ok(())
}
fn handle_supports(sub_args: &ArgMatches) -> ! {
let renderer = sub_args
.get_one::<String>("renderer")
.expect("Required argument");
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: ®ex::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: ®ex::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);
});
}