use anyhow::{Context, Result};mod ai_discovery; use ai_discovery::{ add_markdown_alternate_link, docs_pages, rewrite_docs_links, write_ai_discovery_artifacts, write_markdown_redirect_aliases, write_pages_redirects,};use mdbook::BookItem;use mdbook::book::{Book, Chapter};use mdbook::preprocess::CmdPreprocessor;use regex::Regex;use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};use std::borrow::Cow;use std::collections::{HashMap, HashSet};use std::io::{self, Read};use std::process;use std::sync::{LazyLock, OnceLock}; 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")}); static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| { load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")}); static KEYMAP_JETBRAINS_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| { load_keymap("keymaps/macos/jetbrains.json").expect("Failed to load JetBrains macOS keymap")}); static KEYMAP_JETBRAINS_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| { load_keymap("keymaps/linux/jetbrains.json").expect("Failed to load JetBrains Linux keymap")}); static ALL_ACTIONS: LazyLock<ActionManifest> = LazyLock::new(load_all_actions); #[derive(Clone, Copy)]#[allow(dead_code)]enum Os { MacOs, Linux, Windows,} #[derive(Clone, Copy)]enum KeymapOverlay { JetBrains,} impl KeymapOverlay { fn parse(name: &str) -> Option<Self> { match name { "jetbrains" => Some(Self::JetBrains), _ => None, } } fn keymap(self, os: Os) -> &'static KeymapFile { match (self, os) { (Self::JetBrains, Os::MacOs) => &KEYMAP_JETBRAINS_MACOS, (Self::JetBrains, Os::Linux | Os::Windows) => &KEYMAP_JETBRAINS_LINUX, } }} const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->"; fn main() -> Result<()> { zlog::init(); zlog::init_output_stderr(); let args = std::env::args().skip(1).collect::<Vec<_>>(); match args.get(0).map(String::as_str) { Some("supports") => { let renderer = args.get(1).expect("Required argument"); let supported = renderer != "not-supported"; if supported { process::exit(0); } else { process::exit(1); } } Some("postprocess") => handle_postprocessing()?, _ => handle_preprocessing()?, } Ok(())} #[derive(Debug, Clone, PartialEq, Eq, Hash)]enum PreprocessorError { ActionNotFound { action_name: String, }, DeprecatedActionUsed { used: String, should_be: String, }, InvalidFrontmatterLine(String), InvalidSettingsJson { file: std::path::PathBuf, line: usize, snippet: String, error: String, }, UnknownKeymapOverlay { overlay_name: String, },} impl PreprocessorError { fn new_for_not_found_action(action_name: String) -> Self { for action in &ALL_ACTIONS.actions { for alias in &action.deprecated_aliases { if alias == action_name.as_str() { return PreprocessorError::DeprecatedActionUsed { used: action_name, should_be: action.name.to_string(), }; } } } PreprocessorError::ActionNotFound { action_name } } fn new_for_invalid_settings_json( chapter: &Chapter, location: usize, snippet: String, error: String, ) -> Self { PreprocessorError::InvalidSettingsJson { file: chapter.path.clone().expect("chapter has path"), line: chapter.content[..location].lines().count() + 1, snippet, error, } }} impl std::fmt::Display for PreprocessorError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PreprocessorError::InvalidFrontmatterLine(line) => { write!(f, "Invalid frontmatter line: {}", line) } PreprocessorError::ActionNotFound { action_name } => { write!(f, "Action not found: {}", action_name) } PreprocessorError::DeprecatedActionUsed { used, should_be } => write!( f, "Deprecated action used: {} should be {}", used, should_be ), PreprocessorError::InvalidSettingsJson { file, line, snippet, error, } => { write!( f, "Invalid settings JSON at {}:{}\nError: {}\n\n{}", file.display(), line, error, snippet ) } PreprocessorError::UnknownKeymapOverlay { overlay_name } => { write!( f, "Unknown keymap overlay: '{}'. Supported overlays: jetbrains", overlay_name ) } } }} 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())?; let mut errors = HashSet::<PreprocessorError>::new(); handle_frontmatter(&mut book, &mut errors); template_big_table_of_actions(&mut book); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); template_and_validate_json_snippets(&mut book, &mut errors)?; if !errors.is_empty() { const ANSI_RED: &str = "\x1b[31m"; const ANSI_RESET: &str = "\x1b[0m"; for error in &errors { eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); } return Err(anyhow::anyhow!("Found {} errors in docs", errors.len())); } serde_json::to_writer(io::stdout(), &book)?; Ok(())} fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap(); for_each_chapter_mut(book, |chapter| { let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| { let frontmatter = caps[1].trim(); let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']); let mut metadata = HashMap::<String, String>::default(); for line in frontmatter.lines() { let Some((name, value)) = line.split_once(':') else { errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( "{}: {}", chapter_breadcrumbs(chapter), line ))); continue; }; let name = name.trim(); let value = value.trim(); metadata.insert(name.to_string(), value.to_string()); } FRONT_MATTER_COMMENT.replace( "{}", &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), ) }); if let Cow::Owned(content) = new_content { chapter.content = content; } });} fn template_big_table_of_actions(book: &mut Book) { for_each_chapter_mut(book, |chapter| { let needle = "{#ACTIONS_TABLE#}"; if let Some(start) = chapter.content.rfind(needle) { chapter.content.replace_range( start..start + needle.len(), &generate_big_table_of_actions(), ); } });} fn format_binding(binding: String) -> String { binding.replace("\\", "\\\\")} fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let regex = Regex::new(r"(?s)\{#kb(?::(\w+))?\s+(.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let overlay_name = caps.get(1).map(|m| m.as_str()); let action = caps[2].trim(); if is_missing_action(action) { errors.insert(PreprocessorError::new_for_not_found_action( action.to_string(), )); return String::new(); } let overlay = if let Some(name) = overlay_name { let Some(overlay) = KeymapOverlay::parse(name) else { errors.insert(PreprocessorError::UnknownKeymapOverlay { overlay_name: name.to_string(), }); return String::new(); }; Some(overlay) } else { None }; let macos_binding = find_binding_with_overlay(Os::MacOs, action, overlay) .unwrap_or_default(); let linux_binding = find_binding_with_overlay(Os::Linux, action, overlay) .unwrap_or_default(); if macos_binding.is_empty() && linux_binding.is_empty() { return "<div>No default binding</div>".to_string(); } let formatted_macos_binding = format_binding(macos_binding); let formatted_linux_binding = format_binding(linux_binding); format!("<kbd class=\"keybinding\">{formatted_macos_binding}|{formatted_linux_binding}</kbd>") }) .into_owned() });} fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let regex = Regex::new(r"(?s)\{#action\s+(.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let name = caps[1].trim(); let Some(action) = find_action_by_name(name) else { if actions_available() { errors.insert(PreprocessorError::new_for_not_found_action( name.to_string(), )); } return format!("<code class=\"hljs\">{}</code>", name); }; format!("<code class=\"hljs\">{}</code>", &action.human_name) }) .into_owned() });} fn find_action_by_name(name: &str) -> Option<&ActionDef> { ALL_ACTIONS .actions .binary_search_by(|action| action.name.as_str().cmp(name)) .ok() .map(|index| &ALL_ACTIONS.actions[index])} fn actions_available() -> bool { !ALL_ACTIONS.actions.is_empty()} fn is_missing_action(name: &str) -> bool { actions_available() && find_action_by_name(name).is_none()} // Find the last binding (in keymap order) for the given action.// Exact action matches are preferred over parameterized variants.fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> { let find = |predicate: &dyn Fn(&str) -> bool| { keymap.sections().rev().find_map(|section| { section.bindings().rev().find_map(|(keystroke, a)| { if predicate(&a.to_string()) { Some(keystroke.to_string()) } else { None } }) }) }; // Look for exact match if let Some(binding) = find(&|a| a == action) { return Some(binding); } // Look for parameterized match find(&|a| name_for_action(a.to_string()) == action)} fn find_binding(os: Os, action: &str) -> Option<String> { let keymap = match os { Os::MacOs => &KEYMAP_MACOS, Os::Linux => &KEYMAP_LINUX, Os::Windows => &KEYMAP_WINDOWS, }; find_binding_in_keymap(keymap, action)} fn find_binding_with_overlay( os: Os, action: &str, overlay: Option<KeymapOverlay>,) -> Option<String> { overlay .and_then(|overlay| find_binding_in_keymap(overlay.keymap(os), action)) .or_else(|| find_binding(os, action))} fn template_and_validate_json_snippets( book: &mut Book, errors: &mut HashSet<PreprocessorError>,) -> Result<()> { let params = SettingsJsonSchemaParams { language_names: &[], font_names: &[], theme_names: &[], icon_theme_names: &[], lsp_adapter_names: &[], action_names: &[], action_documentation: &HashMap::default(), deprecations: &HashMap::default(), deprecation_messages: &HashMap::default(), }; let settings_schema = SettingsStore::json_schema(¶ms); let settings_validator = jsonschema::validator_for(&settings_schema) .context("failed to compile settings JSON schema")?; // The keymap schema is built from the action manifest. When `actions.json` // is unavailable (e.g. when running outside of CI without first generating // it) there are no actions, which produces an invalid schema (an empty // `anyOf`). In that case we skip keymap snippet validation rather than // panicking, consistent with the action validation skipped above. let keymap_validator = if actions_available() { let keymap_schema = keymap_schema_for_actions(&ALL_ACTIONS.actions, &ALL_ACTIONS.schema_definitions); Some( jsonschema::validator_for(&keymap_schema) .context("failed to compile keymap JSON schema")?, ) } else { None }; fn for_each_labeled_code_block_mut( book: &mut Book, errors: &mut HashSet<PreprocessorError>, f: &dyn Fn(&str, &str) -> anyhow::Result<()>, ) { const TAGGED_JSON_BLOCK_START: &'static str = "```json ["; const JSON_BLOCK_END: &'static str = "```"; for_each_chapter_mut(book, |chapter| { let mut offset = 0; while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) { let loc = loc + offset; let tag_start = loc + TAGGED_JSON_BLOCK_START.len(); offset = tag_start; let Some(tag_end) = chapter.content[tag_start..].find(']') else { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..tag_start].to_string(), "Unclosed JSON block tag".to_string(), )); continue; }; let tag_end = tag_end + tag_start; let tag = &chapter.content[tag_start..tag_end]; if tag.contains('\n') { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..tag_start].to_string(), "Unclosed JSON block tag".to_string(), )); continue; } let snippet_start = tag_end + 1; offset = snippet_start; let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END) else { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..tag_end + 1].to_string(), "Missing closing code block".to_string(), )); continue; }; let snippet_end = snippet_start + snippet_end; let snippet_json = &chapter.content[snippet_start..snippet_end]; offset = snippet_end + 3; if let Err(err) = f(tag, snippet_json) { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..snippet_end + 3].to_string(), err.to_string(), )); continue; }; let tag_range_complete = tag_start - 1..tag_end + 1; offset -= tag_range_complete.len(); chapter.content.replace_range(tag_range_complete, ""); } }); } for_each_labeled_code_block_mut(book, errors, &|label, snippet_json| { let mut snippet_json_fixed = snippet_json .to_string() .replace("\n>", "\n") .trim() .to_string(); while snippet_json_fixed.starts_with("//") { if let Some(line_end) = snippet_json_fixed.find('\n') {Lines 1–500 of 951. Load more
use anyhow::{Context, Result}; mod ai_discovery; use ai_discovery::{ add_markdown_alternate_link, docs_pages, rewrite_docs_links, write_ai_discovery_artifacts, write_markdown_redirect_aliases, write_pages_redirects, }; use mdbook::BookItem; use mdbook::book::{Book, Chapter}; use mdbook::preprocess::CmdPreprocessor; use regex::Regex; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::io::{self, Read}; use std::process; use std::sync::{LazyLock, OnceLock}; 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") }); static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| { load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") }); static KEYMAP_JETBRAINS_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| { load_keymap("keymaps/macos/jetbrains.json").expect("Failed to load JetBrains macOS keymap") }); static KEYMAP_JETBRAINS_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| { load_keymap("keymaps/linux/jetbrains.json").expect("Failed to load JetBrains Linux keymap") }); static ALL_ACTIONS: LazyLock<ActionManifest> = LazyLock::new(load_all_actions); #[derive(Clone, Copy)] #[allow(dead_code)] enum Os { MacOs, Linux, Windows, } #[derive(Clone, Copy)] enum KeymapOverlay { JetBrains, } impl KeymapOverlay { fn parse(name: &str) -> Option<Self> { match name { "jetbrains" => Some(Self::JetBrains), _ => None, } } fn keymap(self, os: Os) -> &'static KeymapFile { match (self, os) { (Self::JetBrains, Os::MacOs) => &KEYMAP_JETBRAINS_MACOS, (Self::JetBrains, Os::Linux | Os::Windows) => &KEYMAP_JETBRAINS_LINUX, } } } const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->"; fn main() -> Result<()> { zlog::init(); zlog::init_output_stderr(); let args = std::env::args().skip(1).collect::<Vec<_>>(); match args.get(0).map(String::as_str) { Some("supports") => { let renderer = args.get(1).expect("Required argument"); let supported = renderer != "not-supported"; if supported { process::exit(0); } else { process::exit(1); } } Some("postprocess") => handle_postprocessing()?, _ => handle_preprocessing()?, } Ok(()) } #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum PreprocessorError { ActionNotFound { action_name: String, }, DeprecatedActionUsed { used: String, should_be: String, }, InvalidFrontmatterLine(String), InvalidSettingsJson { file: std::path::PathBuf, line: usize, snippet: String, error: String, }, UnknownKeymapOverlay { overlay_name: String, }, } impl PreprocessorError { fn new_for_not_found_action(action_name: String) -> Self { for action in &ALL_ACTIONS.actions { for alias in &action.deprecated_aliases { if alias == action_name.as_str() { return PreprocessorError::DeprecatedActionUsed { used: action_name, should_be: action.name.to_string(), }; } } } PreprocessorError::ActionNotFound { action_name } } fn new_for_invalid_settings_json( chapter: &Chapter, location: usize, snippet: String, error: String, ) -> Self { PreprocessorError::InvalidSettingsJson { file: chapter.path.clone().expect("chapter has path"), line: chapter.content[..location].lines().count() + 1, snippet, error, } } } impl std::fmt::Display for PreprocessorError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PreprocessorError::InvalidFrontmatterLine(line) => { write!(f, "Invalid frontmatter line: {}", line) } PreprocessorError::ActionNotFound { action_name } => { write!(f, "Action not found: {}", action_name) } PreprocessorError::DeprecatedActionUsed { used, should_be } => write!( f, "Deprecated action used: {} should be {}", used, should_be ), PreprocessorError::InvalidSettingsJson { file, line, snippet, error, } => { write!( f, "Invalid settings JSON at {}:{}\nError: {}\n\n{}", file.display(), line, error, snippet ) } PreprocessorError::UnknownKeymapOverlay { overlay_name } => { write!( f, "Unknown keymap overlay: '{}'. Supported overlays: jetbrains", overlay_name ) } } } } 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())?; let mut errors = HashSet::<PreprocessorError>::new(); handle_frontmatter(&mut book, &mut errors); template_big_table_of_actions(&mut book); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); template_and_validate_json_snippets(&mut book, &mut errors)?; if !errors.is_empty() { const ANSI_RED: &str = "\x1b[31m"; const ANSI_RESET: &str = "\x1b[0m"; for error in &errors { eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); } return Err(anyhow::anyhow!("Found {} errors in docs", errors.len())); } serde_json::to_writer(io::stdout(), &book)?; Ok(()) } fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap(); for_each_chapter_mut(book, |chapter| { let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| { let frontmatter = caps[1].trim(); let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']); let mut metadata = HashMap::<String, String>::default(); for line in frontmatter.lines() { let Some((name, value)) = line.split_once(':') else { errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( "{}: {}", chapter_breadcrumbs(chapter), line ))); continue; }; let name = name.trim(); let value = value.trim(); metadata.insert(name.to_string(), value.to_string()); } FRONT_MATTER_COMMENT.replace( "{}", &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), ) }); if let Cow::Owned(content) = new_content { chapter.content = content; } }); } fn template_big_table_of_actions(book: &mut Book) { for_each_chapter_mut(book, |chapter| { let needle = "{#ACTIONS_TABLE#}"; if let Some(start) = chapter.content.rfind(needle) { chapter.content.replace_range( start..start + needle.len(), &generate_big_table_of_actions(), ); } }); } fn format_binding(binding: String) -> String { binding.replace("\\", "\\\\") } fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let regex = Regex::new(r"(?s)\{#kb(?::(\w+))?\s+(.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let overlay_name = caps.get(1).map(|m| m.as_str()); let action = caps[2].trim(); if is_missing_action(action) { errors.insert(PreprocessorError::new_for_not_found_action( action.to_string(), )); return String::new(); } let overlay = if let Some(name) = overlay_name { let Some(overlay) = KeymapOverlay::parse(name) else { errors.insert(PreprocessorError::UnknownKeymapOverlay { overlay_name: name.to_string(), }); return String::new(); }; Some(overlay) } else { None }; let macos_binding = find_binding_with_overlay(Os::MacOs, action, overlay) .unwrap_or_default(); let linux_binding = find_binding_with_overlay(Os::Linux, action, overlay) .unwrap_or_default(); if macos_binding.is_empty() && linux_binding.is_empty() { return "<div>No default binding</div>".to_string(); } let formatted_macos_binding = format_binding(macos_binding); let formatted_linux_binding = format_binding(linux_binding); format!("<kbd class=\"keybinding\">{formatted_macos_binding}|{formatted_linux_binding}</kbd>") }) .into_owned() }); } fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let regex = Regex::new(r"(?s)\{#action\s+(.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let name = caps[1].trim(); let Some(action) = find_action_by_name(name) else { if actions_available() { errors.insert(PreprocessorError::new_for_not_found_action( name.to_string(), )); } return format!("<code class=\"hljs\">{}</code>", name); }; format!("<code class=\"hljs\">{}</code>", &action.human_name) }) .into_owned() }); } fn find_action_by_name(name: &str) -> Option<&ActionDef> { ALL_ACTIONS .actions .binary_search_by(|action| action.name.as_str().cmp(name)) .ok() .map(|index| &ALL_ACTIONS.actions[index]) } fn actions_available() -> bool { !ALL_ACTIONS.actions.is_empty() } fn is_missing_action(name: &str) -> bool { actions_available() && find_action_by_name(name).is_none() } // Find the last binding (in keymap order) for the given action. // Exact action matches are preferred over parameterized variants. fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> { let find = |predicate: &dyn Fn(&str) -> bool| { keymap.sections().rev().find_map(|section| { section.bindings().rev().find_map(|(keystroke, a)| { if predicate(&a.to_string()) { Some(keystroke.to_string()) } else { None } }) }) }; // Look for exact match if let Some(binding) = find(&|a| a == action) { return Some(binding); } // Look for parameterized match find(&|a| name_for_action(a.to_string()) == action) } fn find_binding(os: Os, action: &str) -> Option<String> { let keymap = match os { Os::MacOs => &KEYMAP_MACOS, Os::Linux => &KEYMAP_LINUX, Os::Windows => &KEYMAP_WINDOWS, }; find_binding_in_keymap(keymap, action) } fn find_binding_with_overlay( os: Os, action: &str, overlay: Option<KeymapOverlay>, ) -> Option<String> { overlay .and_then(|overlay| find_binding_in_keymap(overlay.keymap(os), action)) .or_else(|| find_binding(os, action)) } fn template_and_validate_json_snippets( book: &mut Book, errors: &mut HashSet<PreprocessorError>, ) -> Result<()> { let params = SettingsJsonSchemaParams { language_names: &[], font_names: &[], theme_names: &[], icon_theme_names: &[], lsp_adapter_names: &[], action_names: &[], action_documentation: &HashMap::default(), deprecations: &HashMap::default(), deprecation_messages: &HashMap::default(), }; let settings_schema = SettingsStore::json_schema(¶ms); let settings_validator = jsonschema::validator_for(&settings_schema) .context("failed to compile settings JSON schema")?; // The keymap schema is built from the action manifest. When `actions.json` // is unavailable (e.g. when running outside of CI without first generating // it) there are no actions, which produces an invalid schema (an empty // `anyOf`). In that case we skip keymap snippet validation rather than // panicking, consistent with the action validation skipped above. let keymap_validator = if actions_available() { let keymap_schema = keymap_schema_for_actions(&ALL_ACTIONS.actions, &ALL_ACTIONS.schema_definitions); Some( jsonschema::validator_for(&keymap_schema) .context("failed to compile keymap JSON schema")?, ) } else { None }; fn for_each_labeled_code_block_mut( book: &mut Book, errors: &mut HashSet<PreprocessorError>, f: &dyn Fn(&str, &str) -> anyhow::Result<()>, ) { const TAGGED_JSON_BLOCK_START: &'static str = "```json ["; const JSON_BLOCK_END: &'static str = "```"; for_each_chapter_mut(book, |chapter| { let mut offset = 0; while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) { let loc = loc + offset; let tag_start = loc + TAGGED_JSON_BLOCK_START.len(); offset = tag_start; let Some(tag_end) = chapter.content[tag_start..].find(']') else { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..tag_start].to_string(), "Unclosed JSON block tag".to_string(), )); continue; }; let tag_end = tag_end + tag_start; let tag = &chapter.content[tag_start..tag_end]; if tag.contains('\n') { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..tag_start].to_string(), "Unclosed JSON block tag".to_string(), )); continue; } let snippet_start = tag_end + 1; offset = snippet_start; let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END) else { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..tag_end + 1].to_string(), "Missing closing code block".to_string(), )); continue; }; let snippet_end = snippet_start + snippet_end; let snippet_json = &chapter.content[snippet_start..snippet_end]; offset = snippet_end + 3; if let Err(err) = f(tag, snippet_json) { errors.insert(PreprocessorError::new_for_invalid_settings_json( chapter, loc, chapter.content[loc..snippet_end + 3].to_string(), err.to_string(), )); continue; }; let tag_range_complete = tag_start - 1..tag_end + 1; offset -= tag_range_complete.len(); chapter.content.replace_range(tag_range_complete, ""); } }); } for_each_labeled_code_block_mut(book, errors, &|label, snippet_json| { let mut snippet_json_fixed = snippet_json .to_string() .replace("\n>", "\n") .trim() .to_string(); while snippet_json_fixed.starts_with("//") { if let Some(line_end) = snippet_json_fixed.find('\n') {