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: ®ex::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: ®ex::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}|{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: ®ex::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 = "workspace::Save";
547/// let action_name = name_for_action(action_as_str);
548/// assert_eq!(action_name, "workspace::Save");
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: ®ex::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#", &litude_key);
729 let contents = contents.replace("#consent_io_instance#", &consent_io_instance);
730 let contents = title_regex()
731 .replace(&contents, |_: ®ex::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("&", "&")
792 .replace("<", "<")
793 .replace(">", ">"),
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}