md_to_tui/
lib.rs

1//// # Usage 
2//// this library implements `MarkdownParsable` for types that implement `ToString` trait.
3//// You can use `parse_markdown` fn to parse markdown to `Text`. 
4//// `parse_markdown` takes `option` of `MdStyle` and returns `Result<Text<'static>, Error>`
5//// ```rust 
6//// example
7//// let md = "
8//// # TODO
9////
10//// - [ ] one
11//// - [ ] two 
12////
13//// [link](http://exp.com)
14////
15//// "
16//// let res = md.parse_markdown(Some(style))
17//// `
18use error::Error;
19use parser::{lexer::Lexer, parser::Parser};
20use ratatui::text::Text;
21use style::style::MdStyle;
22mod error;
23mod parser;
24pub mod style;
25
26
27/// trait MarkdownParsable will take any trait that impl `ToString` and parse it into ratatui Text
28pub trait MarkdownParsable {
29    /// Convert type to Text
30    fn parse_markdown(&self, style: Option<MdStyle>) -> Result<Text<'static>, Error>;
31}
32
33impl<T> MarkdownParsable for T where T: ToString {
34    fn parse_markdown(&self, style: Option<MdStyle>)  -> Result<Text<'static>, Error> {
35        let mut lexer = Lexer::new();
36        let res =  lexer.parse(self)?;
37
38        let mut parser = Parser::new(res, style);
39        let res = parser.parse()?;
40
41        Ok(Text::from(res))
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use std::{
49    error::Error,
50    io,
51    time::{Duration, Instant}, fs,
52};
53
54use crossterm::{
55    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
56    execute,
57    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
58};
59use ratatui::{
60    backend::{Backend, CrosstermBackend},
61    style::{Color, Modifier, Style},
62    text::Span,
63    widgets::{Block, Borders, Paragraph, Wrap},
64    Frame, Terminal,
65};
66
67
68struct App {
69    scroll: u16,
70}
71
72impl App {
73    fn new() -> App {
74        App { scroll: 0 }
75    }
76
77    fn on_tick(&mut self) {
78        self.scroll += 1;
79        self.scroll %= 10;
80    }
81}
82#[test]
83#[ignore = "github action can't run tui"]
84fn ui_test() -> Result<(), Box<dyn Error>> {
85    // setup terminal
86    enable_raw_mode()?;
87    let mut stdout = io::stdout();
88    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
89    let backend = CrosstermBackend::new(stdout);
90    let mut terminal = Terminal::new(backend)?;
91
92    // create app and run it
93    let tick_rate = Duration::from_millis(250);
94    let app = App::new();
95    let res = run_app(&mut terminal, app, tick_rate);
96
97    // restore terminal
98    disable_raw_mode()?;
99    execute!(
100        terminal.backend_mut(),
101        LeaveAlternateScreen,
102        DisableMouseCapture
103    )?;
104    terminal.show_cursor()?;
105
106    if let Err(err) = res {
107        println!("{err:?}");
108    }
109
110    assert_eq!(true, true);
111    Ok(())
112}
113
114fn run_app<B: Backend>(
115    terminal: &mut Terminal<B>,
116    mut app: App,
117    tick_rate: Duration,
118) -> io::Result<()> {
119    let mut last_tick = Instant::now();
120    loop {
121        terminal.draw(|f| ui(f, &app))?;
122
123        let timeout = tick_rate
124            .checked_sub(last_tick.elapsed())
125            .unwrap_or_else(|| Duration::from_secs(0));
126        if crossterm::event::poll(timeout)? {
127            if let Event::Key(key) = event::read()? {
128                if let KeyCode::Char('q') = key.code {
129                    return Ok(());
130                }
131            }
132        }
133        if last_tick.elapsed() >= tick_rate {
134            app.on_tick();
135            last_tick = Instant::now();
136        }
137    }
138}
139
140#[allow(dead_code)]
141fn ui<B: Backend>(f: &mut Frame<B>, _app: &App) {
142    let size = f.size();
143
144    // Words made "loooong" to demonstrate line breaking.
145    let file = fs::read("src/test/test.md").unwrap();
146    let s = "
147## TODO
1481. otehu
149
150> toehu
151>> aeouth
152___
153[lol](pog.com) lool
154- long_line
155- long_line
156* toue";
157    let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
158    long_line.push('\n');
159
160    let block = Block::default().style(Style::default().fg(Color::Black));
161    f.render_widget(block, size);
162
163    let text =  match String::from_utf8(file).unwrap().parse_markdown(None) {
164        Ok(text) => text,
165        Err(err) => Text::from(err.to_string())
166    };
167        
168    let create_block = |title| {
169        Block::default()
170            .borders(Borders::ALL)
171            .style(Style::default().fg(Color::Gray))
172            .title(Span::styled(
173                title,
174                Style::default().add_modifier(Modifier::BOLD),
175            ))
176    };
177
178
179
180    let paragraph = Paragraph::new(text.clone())
181        .style(Style::default().fg(Color::Gray))
182        .block(create_block("Default alignment (Left), with wrap"))
183        .wrap(Wrap { trim: true });
184    f.render_widget(paragraph, f.size());
185}
186
187}