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:
parent
57424e4743
commit
b057b4697f
7 changed files with 110 additions and 229 deletions
|
@ -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"
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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: ®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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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: ®ex::Captures| {
|
|
||||||
let args = self.parse_args(&caps[1]);
|
|
||||||
self.render(context, &args)
|
|
||||||
})
|
|
||||||
.into_owned()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue