Skip to main content

mdv/
lib.rs

1mod callout;
2mod checkbox;
3mod checkbox_override;
4pub mod cli;
5pub mod config;
6mod custom_code_block;
7mod editor;
8pub mod error;
9mod list_marker;
10pub mod markdown;
11pub mod math;
12pub mod monitor;
13mod pager;
14pub mod renderer;
15pub mod table;
16pub mod terminal;
17pub mod theme;
18mod user_themes;
19pub mod utils;
20
21use anyhow::Result;
22use clap::ArgMatches;
23use cli::Cli;
24use config::Config;
25use markdown::MarkdownProcessor;
26use renderer::TerminalRenderer;
27use std::io::IsTerminal;
28use std::io::{self, Read};
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31
32/// Main entry point for the mdv application
33pub fn run(mut cli: Cli, matches: &ArgMatches) -> Result<()> {
34    if cli.init_config.is_some() {
35        let path = Config::write_default_config(&cli, matches)?;
36        println!("Created config file: {}", path.display());
37        return Ok(());
38    }
39
40    let config = Config::from_cli(&cli, matches)?;
41    if let Some(Some(path)) = &cli.theme_info
42        && cli.filename.is_none()
43    {
44        cli.filename = Some(path.to_string_lossy().into_owned());
45    }
46
47    if matches!(cli.theme_info, Some(None)) {
48        let theme_manager = renderer::terminal::build_theme_manager(&config);
49        print!("{}", format_current_themes(&config));
50        println!();
51        theme::list_themes(&theme_manager);
52        return Ok(());
53    }
54
55    let show_current_theme = config.theme_info || cli.theme_info.is_some();
56
57    let content = get_input_content(&cli)?;
58    let stdout_is_terminal = std::io::stdout().is_terminal();
59    let output = render_document(
60        &content,
61        &config,
62        cli.do_html,
63        show_current_theme,
64        stdout_is_terminal,
65    )?;
66
67    let pager_active = cli.pager && stdout_is_terminal;
68    if pager_active {
69        let pager_file = cli
70            .filename
71            .as_deref()
72            .filter(|filename| *filename != "-")
73            .map(PathBuf::from);
74        let refresh = pager_file.as_ref().map(|path| {
75            let path = path.clone();
76            let config = config.clone();
77            let do_html = cli.do_html;
78            Arc::new(move || render_document_file(&path, &config, do_html, show_current_theme))
79                as pager::RefreshCallback
80        });
81        pager::page(output, pager_file, refresh)?;
82    } else {
83        print!("{}", output);
84    }
85
86    if cli.monitor_file
87        && !pager_active
88        && let Some(filename) = &cli.filename
89    {
90        monitor::watch_file(filename, &config)?;
91    }
92
93    Ok(())
94}
95
96fn render_document(
97    content: &str,
98    config: &Config,
99    do_html: bool,
100    show_current_theme: bool,
101    add_leading_blank: bool,
102) -> Result<String> {
103    let processor = MarkdownProcessor::new(config);
104    let events = processor.parse(content)?;
105    let renderer = TerminalRenderer::new(config)?;
106
107    if do_html {
108        return renderer.to_html(events);
109    }
110
111    let mut output = String::new();
112    if show_current_theme {
113        output.push_str(&format_current_themes(config));
114    }
115    if add_leading_blank {
116        output.push('\n');
117    }
118    output.push_str(&renderer.render(events)?);
119    Ok(output)
120}
121
122fn render_document_file(
123    path: &Path,
124    config: &Config,
125    do_html: bool,
126    show_current_theme: bool,
127) -> Result<String> {
128    let mut content = std::fs::read_to_string(path)?;
129    strip_leading_bom(&mut content);
130    render_document(&content, config, do_html, show_current_theme, true)
131}
132
133fn format_current_themes(config: &Config) -> String {
134    let mut result = String::new();
135    result.push('\n');
136    result.push_str(&format!("Current theme: {}\n", config.theme));
137    result.push_str(&format!(
138        "Current code theme: {}\n",
139        config.code_theme.as_deref().unwrap_or(&config.theme)
140    ));
141    result
142}
143
144fn get_input_content(cli: &Cli) -> Result<String> {
145    let mut content = match &cli.filename {
146        Some(filename) if filename == "-" => {
147            let mut content = String::new();
148            io::stdin().read_to_string(&mut content)?;
149            content
150        }
151        Some(filename) => {
152            let path = Path::new(filename);
153            if !path.exists() {
154                anyhow::bail!("File not found: {}", filename);
155            }
156            std::fs::read_to_string(path)?
157        }
158        None => {
159            if io::stdin().is_terminal() {
160                anyhow::bail!("No input file: provide a file path or pipe content via stdin");
161            }
162            let mut content = String::new();
163            io::stdin().read_to_string(&mut content)?;
164            content
165        }
166    };
167
168    strip_leading_bom(&mut content);
169    Ok(content)
170}
171
172fn strip_leading_bom(text: &mut String) {
173    const UTF8_BOM: char = '\u{FEFF}';
174    while text.starts_with(UTF8_BOM) {
175        // Standard PowerShell adds a UTF-8 BOM when piping text.
176        let bom_len = UTF8_BOM.len_utf8();
177        text.drain(..bom_len);
178    }
179}