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
32pub 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 let bom_len = UTF8_BOM.len_utf8();
177 text.drain(..bom_len);
178 }
179}