main.rs

  1use anyhow::{Context, Result};
  2use mdbook::BookItem;
  3use mdbook::book::{Book, Chapter};
  4use mdbook::preprocess::CmdPreprocessor;
  5use regex::Regex;
  6use settings::{KeymapFile, SettingsStore};
  7use std::borrow::Cow;
  8use std::collections::{HashMap, HashSet};
  9use std::io::{self, Read};
 10use std::process;
 11use std::sync::{LazyLock, OnceLock};
 12
 13static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
 14    load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
 15});
 16
 17static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
 18    load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
 19});
 20
 21static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
 22    load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
 23});
 24
 25static KEYMAP_JETBRAINS_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
 26    load_keymap("keymaps/macos/jetbrains.json").expect("Failed to load JetBrains macOS keymap")
 27});
 28
 29static KEYMAP_JETBRAINS_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
 30    load_keymap("keymaps/linux/jetbrains.json").expect("Failed to load JetBrains Linux keymap")
 31});
 32
 33static ALL_ACTIONS: LazyLock<ActionManifest> = LazyLock::new(load_all_actions);
 34
 35#[derive(Clone, Copy)]
 36#[allow(dead_code)]
 37enum Os {
 38    MacOs,
 39    Linux,
 40    Windows,
 41}
 42
 43#[derive(Clone, Copy)]
 44enum KeymapOverlay {
 45    JetBrains,
 46}
 47
 48impl KeymapOverlay {
 49    fn parse(name: &str) -> Option<Self> {
 50        match name {
 51            "jetbrains" => Some(Self::JetBrains),
 52            _ => None,
 53        }
 54    }
 55
 56    fn keymap(self, os: Os) -> &'static KeymapFile {
 57        match (self, os) {
 58            (Self::JetBrains, Os::MacOs) => &KEYMAP_JETBRAINS_MACOS,
 59            (Self::JetBrains, Os::Linux | Os::Windows) => &KEYMAP_JETBRAINS_LINUX,
 60        }
 61    }
 62}
 63
 64const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
 65
 66fn main() -> Result<()> {
 67    zlog::init();
 68    zlog::init_output_stderr();
 69    let args = std::env::args().skip(1).collect::<Vec<_>>();
 70
 71    match args.get(0).map(String::as_str) {
 72        Some("supports") => {
 73            let renderer = args.get(1).expect("Required argument");
 74            let supported = renderer != "not-supported";
 75            if supported {
 76                process::exit(0);
 77            } else {
 78                process::exit(1);
 79            }
 80        }
 81        Some("postprocess") => handle_postprocessing()?,
 82        _ => handle_preprocessing()?,
 83    }
 84
 85    Ok(())
 86}
 87
 88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 89enum PreprocessorError {
 90    ActionNotFound {
 91        action_name: String,
 92    },
 93    DeprecatedActionUsed {
 94        used: String,
 95        should_be: String,
 96    },
 97    InvalidFrontmatterLine(String),
 98    InvalidSettingsJson {
 99        file: std::path::PathBuf,
100        line: usize,
101        snippet: String,
102        error: String,
103    },
104    UnknownKeymapOverlay {
105        overlay_name: String,
106    },
107}
108
109impl PreprocessorError {
110    fn new_for_not_found_action(action_name: String) -> Self {
111        for action in &ALL_ACTIONS.actions {
112            for alias in &action.deprecated_aliases {
113                if alias == action_name.as_str() {
114                    return PreprocessorError::DeprecatedActionUsed {
115                        used: action_name,
116                        should_be: action.name.to_string(),
117                    };
118                }
119            }
120        }
121        PreprocessorError::ActionNotFound { action_name }
122    }
123
124    fn new_for_invalid_settings_json(
125        chapter: &Chapter,
126        location: usize,
127        snippet: String,
128        error: String,
129    ) -> Self {
130        PreprocessorError::InvalidSettingsJson {
131            file: chapter.path.clone().expect("chapter has path"),
132            line: chapter.content[..location].lines().count() + 1,
133            snippet,
134            error,
135        }
136    }
137}
138
139impl std::fmt::Display for PreprocessorError {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            PreprocessorError::InvalidFrontmatterLine(line) => {
143                write!(f, "Invalid frontmatter line: {}", line)
144            }
145            PreprocessorError::ActionNotFound { action_name } => {
146                write!(f, "Action not found: {}", action_name)
147            }
148            PreprocessorError::DeprecatedActionUsed { used, should_be } => write!(
149                f,
150                "Deprecated action used: {} should be {}",
151                used, should_be
152            ),
153            PreprocessorError::InvalidSettingsJson {
154                file,
155                line,
156                snippet,
157                error,
158            } => {
159                write!(
160                    f,
161                    "Invalid settings JSON at {}:{}\nError: {}\n\n{}",
162                    file.display(),
163                    line,
164                    error,
165                    snippet
166                )
167            }
168            PreprocessorError::UnknownKeymapOverlay { overlay_name } => {
169                write!(
170                    f,
171                    "Unknown keymap overlay: '{}'. Supported overlays: jetbrains",
172                    overlay_name
173                )
174            }
175        }
176    }
177}
178
179fn handle_preprocessing() -> Result<()> {
180    let mut stdin = io::stdin();
181    let mut input = String::new();
182    stdin.read_to_string(&mut input)?;
183
184    let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
185
186    let mut errors = HashSet::<PreprocessorError>::new();
187    handle_frontmatter(&mut book, &mut errors);
188    template_big_table_of_actions(&mut book);
189    template_and_validate_keybindings(&mut book, &mut errors);
190    template_and_validate_actions(&mut book, &mut errors);
191    template_and_validate_json_snippets(&mut book, &mut errors);
192
193    if !errors.is_empty() {
194        const ANSI_RED: &str = "\x1b[31m";
195        const ANSI_RESET: &str = "\x1b[0m";
196        for error in &errors {
197            eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error);
198        }
199        return Err(anyhow::anyhow!("Found {} errors in docs", errors.len()));
200    }
201
202    serde_json::to_writer(io::stdout(), &book)?;
203
204    Ok(())
205}
206
207fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
208    let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap();
209    for_each_chapter_mut(book, |chapter| {
210        let new_content = frontmatter_regex.replace(&chapter.content, |caps: &regex::Captures| {
211            let frontmatter = caps[1].trim();
212            let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']);
213            let mut metadata = HashMap::<String, String>::default();
214            for line in frontmatter.lines() {
215                let Some((name, value)) = line.split_once(':') else {
216                    errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
217                        "{}: {}",
218                        chapter_breadcrumbs(chapter),
219                        line
220                    )));
221                    continue;
222                };
223                let name = name.trim();
224                let value = value.trim();
225                metadata.insert(name.to_string(), value.to_string());
226            }
227            FRONT_MATTER_COMMENT.replace(
228                "{}",
229                &serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
230            )
231        });
232        if let Cow::Owned(content) = new_content {
233            chapter.content = content;
234        }
235    });
236}
237
238fn template_big_table_of_actions(book: &mut Book) {
239    for_each_chapter_mut(book, |chapter| {
240        let needle = "{#ACTIONS_TABLE#}";
241        if let Some(start) = chapter.content.rfind(needle) {
242            chapter.content.replace_range(
243                start..start + needle.len(),
244                &generate_big_table_of_actions(),
245            );
246        }
247    });
248}
249
250fn format_binding(binding: String) -> String {
251    binding.replace("\\", "\\\\")
252}
253
254fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
255    let regex = Regex::new(r"\{#kb(?::(\w+))?\s+(.*?)\}").unwrap();
256
257    for_each_chapter_mut(book, |chapter| {
258        chapter.content = regex
259            .replace_all(&chapter.content, |caps: &regex::Captures| {
260                let overlay_name = caps.get(1).map(|m| m.as_str());
261                let action = caps[2].trim();
262
263                if is_missing_action(action) {
264                    errors.insert(PreprocessorError::new_for_not_found_action(
265                        action.to_string(),
266                    ));
267                    return String::new();
268                }
269
270                let overlay = if let Some(name) = overlay_name {
271                    let Some(overlay) = KeymapOverlay::parse(name) else {
272                        errors.insert(PreprocessorError::UnknownKeymapOverlay {
273                            overlay_name: name.to_string(),
274                        });
275                        return String::new();
276                    };
277                    Some(overlay)
278                } else {
279                    None
280                };
281
282                let macos_binding =
283                    find_binding_with_overlay(Os::MacOs, action, overlay)
284                        .unwrap_or_default();
285                let linux_binding =
286                    find_binding_with_overlay(Os::Linux, action, overlay)
287                        .unwrap_or_default();
288
289                if macos_binding.is_empty() && linux_binding.is_empty() {
290                    return "<div>No default binding</div>".to_string();
291                }
292
293                let formatted_macos_binding = format_binding(macos_binding);
294                let formatted_linux_binding = format_binding(linux_binding);
295
296                format!("<kbd class=\"keybinding\">{formatted_macos_binding}&#124;{formatted_linux_binding}</kbd>")
297            })
298            .into_owned()
299    });
300}
301
302fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
303    let regex = Regex::new(r"\{#action (.*?)\}").unwrap();
304
305    for_each_chapter_mut(book, |chapter| {
306        chapter.content = regex
307            .replace_all(&chapter.content, |caps: &regex::Captures| {
308                let name = caps[1].trim();
309                let Some(action) = find_action_by_name(name) else {
310                    if actions_available() {
311                        errors.insert(PreprocessorError::new_for_not_found_action(
312                            name.to_string(),
313                        ));
314                    }
315                    return format!("<code class=\"hljs\">{}</code>", name);
316                };
317                format!("<code class=\"hljs\">{}</code>", &action.human_name)
318            })
319            .into_owned()
320    });
321}
322
323fn find_action_by_name(name: &str) -> Option<&ActionDef> {
324    ALL_ACTIONS
325        .actions
326        .binary_search_by(|action| action.name.as_str().cmp(name))
327        .ok()
328        .map(|index| &ALL_ACTIONS.actions[index])
329}
330
331fn actions_available() -> bool {
332    !ALL_ACTIONS.actions.is_empty()
333}
334
335fn is_missing_action(name: &str) -> bool {
336    actions_available() && find_action_by_name(name).is_none()
337}
338
339// Find the binding in reverse order, as the last binding takes precedence.
340fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> {
341    keymap.sections().rev().find_map(|section| {
342        section.bindings().rev().find_map(|(keystroke, a)| {
343            if name_for_action(a.to_string()) == action {
344                Some(keystroke.to_string())
345            } else {
346                None
347            }
348        })
349    })
350}
351
352fn find_binding(os: Os, action: &str) -> Option<String> {
353    let keymap = match os {
354        Os::MacOs => &KEYMAP_MACOS,
355        Os::Linux => &KEYMAP_LINUX,
356        Os::Windows => &KEYMAP_WINDOWS,
357    };
358    find_binding_in_keymap(keymap, action)
359}
360
361fn find_binding_with_overlay(
362    os: Os,
363    action: &str,
364    overlay: Option<KeymapOverlay>,
365) -> Option<String> {
366    overlay
367        .and_then(|overlay| find_binding_in_keymap(overlay.keymap(os), action))
368        .or_else(|| find_binding(os, action))
369}
370
371fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
372    let settings_schema = SettingsStore::json_schema(&Default::default());
373    let settings_validator = jsonschema::validator_for(&settings_schema)
374        .expect("failed to compile settings JSON schema");
375
376    let keymap_schema =
377        keymap_schema_for_actions(&ALL_ACTIONS.actions, &ALL_ACTIONS.schema_definitions);
378    let keymap_validator =
379        jsonschema::validator_for(&keymap_schema).expect("failed to compile keymap JSON schema");
380
381    fn for_each_labeled_code_block_mut(
382        book: &mut Book,
383        errors: &mut HashSet<PreprocessorError>,
384        f: &dyn Fn(&str, &str) -> anyhow::Result<()>,
385    ) {
386        const TAGGED_JSON_BLOCK_START: &'static str = "```json [";
387        const JSON_BLOCK_END: &'static str = "```";
388
389        for_each_chapter_mut(book, |chapter| {
390            let mut offset = 0;
391            while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) {
392                let loc = loc + offset;
393                let tag_start = loc + TAGGED_JSON_BLOCK_START.len();
394                offset = tag_start;
395                let Some(tag_end) = chapter.content[tag_start..].find(']') else {
396                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
397                        chapter,
398                        loc,
399                        chapter.content[loc..tag_start].to_string(),
400                        "Unclosed JSON block tag".to_string(),
401                    ));
402                    continue;
403                };
404                let tag_end = tag_end + tag_start;
405
406                let tag = &chapter.content[tag_start..tag_end];
407
408                if tag.contains('\n') {
409                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
410                        chapter,
411                        loc,
412                        chapter.content[loc..tag_start].to_string(),
413                        "Unclosed JSON block tag".to_string(),
414                    ));
415                    continue;
416                }
417
418                let snippet_start = tag_end + 1;
419                offset = snippet_start;
420
421                let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END)
422                else {
423                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
424                        chapter,
425                        loc,
426                        chapter.content[loc..tag_end + 1].to_string(),
427                        "Missing closing code block".to_string(),
428                    ));
429                    continue;
430                };
431                let snippet_end = snippet_start + snippet_end;
432                let snippet_json = &chapter.content[snippet_start..snippet_end];
433                offset = snippet_end + 3;
434
435                if let Err(err) = f(tag, snippet_json) {
436                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
437                        chapter,
438                        loc,
439                        chapter.content[loc..snippet_end + 3].to_string(),
440                        err.to_string(),
441                    ));
442                    continue;
443                };
444                let tag_range_complete = tag_start - 1..tag_end + 1;
445                offset -= tag_range_complete.len();
446                chapter.content.replace_range(tag_range_complete, "");
447            }
448        });
449    }
450
451    for_each_labeled_code_block_mut(book, errors, &|label, snippet_json| {
452        let mut snippet_json_fixed = snippet_json
453            .to_string()
454            .replace("\n>", "\n")
455            .trim()
456            .to_string();
457        while snippet_json_fixed.starts_with("//") {
458            if let Some(line_end) = snippet_json_fixed.find('\n') {
459                snippet_json_fixed.replace_range(0..line_end, "");
460                snippet_json_fixed = snippet_json_fixed.trim().to_string();
461            }
462        }
463        match label {
464            "settings" => {
465                if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
466                    snippet_json_fixed.insert(0, '{');
467                    snippet_json_fixed.push_str("\n}");
468                }
469                let value =
470                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;
471                let validation_errors: Vec<String> = settings_validator
472                    .iter_errors(&value)
473                    .map(|err| err.to_string())
474                    .collect();
475                if !validation_errors.is_empty() {
476                    anyhow::bail!("{}", validation_errors.join("\n"));
477                }
478            }
479            "keymap" => {
480                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
481                    snippet_json_fixed.insert(0, '[');
482                    snippet_json_fixed.push_str("\n]");
483                }
484
485                let value =
486                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;
487                let validation_errors: Vec<String> = keymap_validator
488                    .iter_errors(&value)
489                    .map(|err| err.to_string())
490                    .collect();
491                if !validation_errors.is_empty() {
492                    anyhow::bail!("{}", validation_errors.join("\n"));
493                }
494            }
495            "debug" => {
496                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
497                    snippet_json_fixed.insert(0, '[');
498                    snippet_json_fixed.push_str("\n]");
499                }
500
501                settings::parse_json_with_comments::<task::DebugTaskFile>(&snippet_json_fixed)?;
502            }
503            "tasks" => {
504                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
505                    snippet_json_fixed.insert(0, '[');
506                    snippet_json_fixed.push_str("\n]");
507                }
508
509                settings::parse_json_with_comments::<task::TaskTemplates>(&snippet_json_fixed)?;
510            }
511            "icon-theme" => {
512                if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
513                    snippet_json_fixed.insert(0, '{');
514                    snippet_json_fixed.push_str("\n}");
515                }
516
517                settings::parse_json_with_comments::<theme::IconThemeFamilyContent>(
518                    &snippet_json_fixed,
519                )?;
520            }
521            "semantic_token_rules" => {
522                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
523                    snippet_json_fixed.insert(0, '[');
524                    snippet_json_fixed.push_str("\n]");
525                }
526
527                settings::parse_json_with_comments::<settings::SemanticTokenRules>(
528                    &snippet_json_fixed,
529                )?;
530            }
531            label => anyhow::bail!("Unexpected JSON code block tag: {label}"),
532        };
533        Ok(())
534    });
535}
536
537/// Removes any configurable options from the stringified action if existing,
538/// ensuring that only the actual action name is returned. If the action consists
539/// only of a string and nothing else, the string is returned as-is.
540///
541/// Example:
542///
543/// This will return the action name unmodified.
544///
545/// ```
546/// let action_as_str = "assistant::Assist";
547/// let action_name = name_for_action(action_as_str);
548/// assert_eq!(action_name, "assistant::Assist");
549/// ```
550///
551/// This will return the action name with any trailing options removed.
552///
553///
554/// ```
555/// let action_as_str = "\"editor::ToggleComments\", {\"advance_downwards\":false}";
556/// let action_name = name_for_action(action_as_str);
557/// assert_eq!(action_name, "editor::ToggleComments");
558/// ```
559fn name_for_action(action_as_str: String) -> String {
560    action_as_str
561        .split(",")
562        .next()
563        .map(|name| name.trim_matches('"').to_string())
564        .unwrap_or(action_as_str)
565}
566
567fn chapter_breadcrumbs(chapter: &Chapter) -> String {
568    let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1);
569    breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str));
570    breadcrumbs.push(chapter.name.as_str());
571    format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > "))
572}
573
574fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
575    let content = util::asset_str::<settings::SettingsAssets>(asset_path);
576    KeymapFile::parse(content.as_ref())
577}
578
579fn for_each_chapter_mut<F>(book: &mut Book, mut func: F)
580where
581    F: FnMut(&mut Chapter),
582{
583    book.for_each_mut(|item| {
584        let BookItem::Chapter(chapter) = item else {
585            return;
586        };
587        func(chapter);
588    });
589}
590
591#[derive(Debug, serde::Serialize, serde::Deserialize)]
592struct ActionDef {
593    name: String,
594    human_name: String,
595    #[serde(default)]
596    schema: Option<serde_json::Value>,
597    deprecated_aliases: Vec<String>,
598    #[serde(default)]
599    deprecation_message: Option<String>,
600    #[serde(rename = "documentation")]
601    docs: Option<String>,
602}
603
604#[derive(Debug, serde::Deserialize)]
605struct ActionManifest {
606    actions: Vec<ActionDef>,
607    #[serde(default)]
608    schema_definitions: serde_json::Map<String, serde_json::Value>,
609}
610
611fn load_all_actions() -> ActionManifest {
612    let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
613    match std::fs::read_to_string(asset_path) {
614        Ok(content) => {
615            let mut manifest: ActionManifest =
616                serde_json::from_str(&content).expect("Failed to parse actions.json");
617            manifest.actions.sort_by(|a, b| a.name.cmp(&b.name));
618            manifest
619        }
620        Err(err) => {
621            if std::env::var("CI").is_ok() {
622                panic!("actions.json not found at {}: {}", asset_path, err);
623            }
624            eprintln!(
625                "Warning: actions.json not found, action validation will be skipped: {}",
626                err
627            );
628            ActionManifest {
629                actions: Vec::new(),
630                schema_definitions: serde_json::Map::new(),
631            }
632        }
633    }
634}
635
636fn handle_postprocessing() -> Result<()> {
637    let logger = zlog::scoped!("render");
638    let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?;
639    let output = ctx
640        .config
641        .get_mut("output")
642        .expect("has output")
643        .as_table_mut()
644        .expect("output is table");
645    let zed_html = output.remove("zed-html").expect("zed-html output defined");
646    let default_description = zed_html
647        .get("default-description")
648        .expect("Default description not found")
649        .as_str()
650        .expect("Default description not a string")
651        .to_string();
652    let default_title = zed_html
653        .get("default-title")
654        .expect("Default title not found")
655        .as_str()
656        .expect("Default title not a string")
657        .to_string();
658    let amplitude_key = std::env::var("DOCS_AMPLITUDE_API_KEY").unwrap_or_default();
659    let consent_io_instance = std::env::var("DOCS_CONSENT_IO_INSTANCE").unwrap_or_default();
660
661    output.insert("html".to_string(), zed_html);
662    mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?;
663    let ignore_list = ["toc.html"];
664
665    let root_dir = ctx.destination.clone();
666    let mut files = Vec::with_capacity(128);
667    let mut queue = Vec::with_capacity(64);
668    queue.push(root_dir.clone());
669    while let Some(dir) = queue.pop() {
670        for entry in std::fs::read_dir(&dir).context("failed to read docs dir")? {
671            let Ok(entry) = entry else {
672                continue;
673            };
674            let file_type = entry.file_type().context("Failed to determine file type")?;
675            if file_type.is_dir() {
676                queue.push(entry.path());
677            }
678            if file_type.is_file()
679                && matches!(
680                    entry.path().extension().and_then(std::ffi::OsStr::to_str),
681                    Some("html")
682                )
683            {
684                if ignore_list.contains(&&*entry.file_name().to_string_lossy()) {
685                    zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy());
686                } else {
687                    files.push(entry.path());
688                }
689            }
690        }
691    }
692
693    zlog::info!(logger => "Processing {} `.html` files", files.len());
694    let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap();
695    for file in files {
696        let contents = std::fs::read_to_string(&file)?;
697        let mut meta_description = None;
698        let mut meta_title = None;
699        let contents = meta_regex.replace(&contents, |caps: &regex::Captures| {
700            let metadata: HashMap<String, String> = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata");
701            for (kind, content) in metadata {
702                match kind.as_str() {
703                    "description" => {
704                        meta_description = Some(content);
705                    }
706                    "title" => {
707                        meta_title = Some(content);
708                    }
709                    _ => {
710                        zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir));
711                    }
712                }
713            }
714            String::new()
715        });
716        let meta_description = meta_description.as_ref().unwrap_or_else(|| {
717            zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir));
718            &default_description
719        });
720        let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir));
721        let meta_title = meta_title.as_ref().unwrap_or_else(|| {
722            zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir));
723            &default_title
724        });
725        let meta_title = format!("{} | {}", page_title, meta_title);
726        zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
727        let contents = contents.replace("#description#", meta_description);
728        let contents = contents.replace("#amplitude_key#", &amplitude_key);
729        let contents = contents.replace("#consent_io_instance#", &consent_io_instance);
730        let contents = title_regex()
731            .replace(&contents, |_: &regex::Captures| {
732                format!("<title>{}</title>", meta_title)
733            })
734            .to_string();
735        // let contents = contents.replace("#title#", &meta_title);
736        std::fs::write(file, contents)?;
737    }
738    return Ok(());
739
740    fn pretty_path<'a>(
741        path: &'a std::path::PathBuf,
742        root: &'a std::path::PathBuf,
743    ) -> &'a std::path::Path {
744        path.strip_prefix(&root).unwrap_or(path)
745    }
746    fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
747        let title_tag_contents = &title_regex()
748            .captures(contents)
749            .with_context(|| format!("Failed to find title in {:?}", pretty_path))
750            .expect("Page has <title> element")[1];
751
752        title_tag_contents
753            .trim()
754            .strip_suffix("- Zed")
755            .unwrap_or(title_tag_contents)
756            .trim()
757            .to_string()
758    }
759}
760
761fn title_regex() -> &'static Regex {
762    static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
763    TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
764}
765
766fn generate_big_table_of_actions() -> String {
767    let actions = &ALL_ACTIONS.actions;
768    let mut output = String::new();
769
770    let mut actions_sorted = actions.iter().collect::<Vec<_>>();
771    actions_sorted.sort_by_key(|a| a.name.as_str());
772
773    // Start the definition list with custom styling for better spacing
774    output.push_str("<dl style=\"line-height: 1.8;\">\n");
775
776    for action in actions_sorted.into_iter() {
777        // Add the humanized action name as the term with margin
778        output.push_str(
779            "<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
780        );
781        output.push_str(&action.human_name);
782        output.push_str("</code></dt>\n");
783
784        // Add the definition with keymap name and description
785        output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
786
787        // Add the description, escaping HTML if needed
788        if let Some(description) = action.docs.as_ref() {
789            output.push_str(
790                &description
791                    .replace("&", "&amp;")
792                    .replace("<", "&lt;")
793                    .replace(">", "&gt;"),
794            );
795            output.push_str("<br>\n");
796        }
797        output.push_str("Keymap Name: <code>");
798        output.push_str(&action.name);
799        output.push_str("</code><br>\n");
800        if !action.deprecated_aliases.is_empty() {
801            output.push_str("Deprecated Alias(es): ");
802            for alias in action.deprecated_aliases.iter() {
803                output.push_str("<code>");
804                output.push_str(alias);
805                output.push_str("</code>, ");
806            }
807        }
808        output.push_str("\n</dd>\n");
809    }
810
811    // Close the definition list
812    output.push_str("</dl>\n");
813
814    output
815}
816
817fn keymap_schema_for_actions(
818    actions: &[ActionDef],
819    schema_definitions: &serde_json::Map<String, serde_json::Value>,
820) -> serde_json::Value {
821    let mut generator = KeymapFile::action_schema_generator();
822
823    for (name, definition) in schema_definitions {
824        generator
825            .definitions_mut()
826            .insert(name.clone(), definition.clone());
827    }
828
829    let mut action_schemas = Vec::new();
830    let mut documentation = collections::HashMap::<&str, &str>::default();
831    let mut deprecations = collections::HashMap::<&str, &str>::default();
832    let mut deprecation_messages = collections::HashMap::<&str, &str>::default();
833
834    for action in actions {
835        let schema = action
836            .schema
837            .as_ref()
838            .and_then(|v| serde_json::from_value::<schemars::Schema>(v.clone()).ok());
839        action_schemas.push((action.name.as_str(), schema));
840        if let Some(doc) = &action.docs {
841            documentation.insert(action.name.as_str(), doc.as_str());
842        }
843        if let Some(msg) = &action.deprecation_message {
844            deprecation_messages.insert(action.name.as_str(), msg.as_str());
845        }
846        for alias in &action.deprecated_aliases {
847            deprecations.insert(alias.as_str(), action.name.as_str());
848            let alias_schema = action
849                .schema
850                .as_ref()
851                .and_then(|v| serde_json::from_value::<schemars::Schema>(v.clone()).ok());
852            action_schemas.push((alias.as_str(), alias_schema));
853        }
854    }
855
856    KeymapFile::generate_json_schema(
857        generator,
858        action_schemas,
859        &documentation,
860        &deprecations,
861        &deprecation_messages,
862    )
863}