tango/
md2rs.rs

1use std::io::{self, BufRead, Write};
2
3pub struct Converter {
4    state: State,
5    blank_line_count: usize,
6    buffered_lines: String,
7    warnings: Vec<Warning>,
8}
9
10use super::Warning;
11
12#[derive(Copy, Clone, PartialEq, Eq, Debug)]
13enum State { MarkdownBlank, MarkdownText, MarkdownMeta, Rust, }
14impl Converter {
15    pub fn new() -> Converter {
16        Converter {
17            state: State::MarkdownBlank,
18            blank_line_count: 0,
19            buffered_lines: String::new(),
20            warnings: vec![],
21        }
22    }
23}
24
25pub enum Exception {
26    IoError(io::Error),
27    Warnings(Vec<Warning>),
28}
29
30impl From<io::Error> for Exception {
31    fn from(e: io::Error) -> Self {
32        Exception::IoError(e)
33    }
34}
35
36impl Converter {
37    pub fn convert<R:io::Read, W:io::Write>(mut self, r:R, mut w:W) -> Result<(), Exception> {
38        let source = io::BufReader::new(r);
39        for line in source.lines() {
40            let line = (line)?;
41            (self.handle(&line, &mut w))?;
42        }
43        if self.warnings.is_empty() {
44            Ok(())
45        } else {
46            Err(Exception::Warnings(self.warnings))
47        }
48    }
49
50    pub fn handle(&mut self, line: &str, w: &mut dyn Write) -> io::Result<()> {
51        let str9 = line.chars().take(9).collect::<String>();
52        let str7 = line.chars().take(7).collect::<String>();
53        match (self.state, &str7[..], &str9[..]) {
54            (State::MarkdownBlank, "```rust", _) |
55            (State::MarkdownText, "```rust", _) => {
56                self.buffered_lines = String::new();
57                let rest =  &line.chars().skip(7).collect::<String>();
58                if rest != "" {
59                    (self.transition(w, State::MarkdownMeta))?;
60                    (self.meta_note(&rest, w))?;
61                }
62                self.transition(w, State::Rust)
63            }
64            (State::MarkdownBlank, _, "```{.rust") |
65            (State::MarkdownText, _, "```{.rust") => {
66                self.buffered_lines = String::new();
67                let rest =  &line.chars().skip(9).collect::<String>();
68                if rest != "" {
69                    (self.transition(w, State::MarkdownMeta))?;
70                    (self.meta_note(&format!(" {{{}", rest), w))?;
71                }
72                self.transition(w, State::Rust)
73            }
74            (State::Rust, "```", _) => {
75                self.transition(w, State::MarkdownBlank)
76            }
77
78            // FIXME: accum blank lines and only emit them with
79            // prefix if there's no state transition; otherwise
80            // emit them with no prefix. (This is in part the
81            // motivation for the `fn finish_section` design.)
82            (_, "", _) => {
83                self.blank_line(w)
84            }
85
86            _ => {
87                // HACK: if we find anything that looks like a markdown-named playpen link ...
88                let open_pat = "[";
89                let close_pat = "]: https://play.rust-lang.org/?code=";
90                if let (Some(open), Some(close)) = (line.find(open_pat), line.find(close_pat)) {
91                    // ... then we assume it is associated with the (hopefully immediately preceding)
92                    // code block, so we emit a `//@@@` named tag for that code block.
93
94                    // checking here that emitted code block matches
95                    // up with emitted url. If non-match, then warn
96                    // the user, and suggest they re-run `tango` after
97                    // touching the file to generate matching url.
98                    let expect = super::encode_to_url(&self.buffered_lines);
99                    let actual = &line[(close+3)..];
100                    if expect != actual {
101                        self.warnings.push(Warning::EncodedUrlMismatch {
102                            actual: actual.to_string(),
103                            expect: expect
104                        })
105                    }
106                    self.name_block(line, &line[open+1..close], w)
107                } else {
108                    self.nonblank_line(line, w)
109                }
110            }
111        }
112    }
113
114    pub fn meta_note(&mut self, note: &str, w: &mut dyn Write) -> io::Result<()> {
115        assert!(note != "");
116        self.nonblank_line(note, w)
117    }
118
119    pub fn name_block(&mut self, _line: &str, name: &str, w: &mut dyn Write) -> io::Result<()> {
120        assert!(name != "");
121        writeln!(w, "//@@@ {}", name)
122    }
123
124    pub fn nonblank_line(&mut self, line: &str, w: &mut dyn Write) -> io::Result<()> {
125        let (blank_prefix, line_prefix) = match self.state {
126            State::MarkdownBlank => ("", "//@ "),
127            State::MarkdownText => ("//@", "//@ "),
128            State::MarkdownMeta => ("//@", "//@@"),
129            State::Rust => ("", ""),
130        };
131        for _ in 0..self.blank_line_count {
132            (writeln!(w, "{}", blank_prefix))?;
133
134        }
135        self.blank_line_count = 0;
136
137        match self.state {
138            State::MarkdownBlank =>
139                (self.transition(w, State::MarkdownText))?,
140            State::MarkdownMeta |
141            State::MarkdownText => {}
142            State::Rust => {
143                self.buffered_lines.push_str("\n");
144                self.buffered_lines.push_str(line);
145            }
146        }
147
148        writeln!(w, "{}{}", line_prefix, line)
149    }
150
151    fn blank_line(&mut self, _w: &mut dyn Write) -> io::Result<()> {
152        match self.state {
153            State::Rust => {
154                self.buffered_lines.push_str("\n");
155            }
156            State::MarkdownBlank |
157            State::MarkdownMeta |
158            State::MarkdownText => {}
159        }
160        self.blank_line_count += 1;
161        Ok(())
162    }
163
164    fn finish_section(&mut self, w: &mut dyn Write) -> io::Result<()> {
165        for _ in 0..self.blank_line_count {
166            (writeln!(w, ""))?;
167        }
168        self.blank_line_count = 0;
169        Ok(())
170    }
171
172    fn transition(&mut self, w: &mut dyn Write, s: State) -> io::Result<()> {
173        match s {
174            State::MarkdownMeta => {
175                assert!(self.state != State::Rust);
176                (self.finish_section(w))?;
177            }
178            State::Rust => {
179                assert!(self.state != State::Rust);
180                self.buffered_lines = String::new();
181            }
182            State::MarkdownText => {
183                assert_eq!(self.state, State::MarkdownBlank);
184                (self.finish_section(w))?;
185            }
186            State::MarkdownBlank => {
187                assert_eq!(self.state, State::Rust);
188                (self.finish_section(w))?;
189            }
190        }
191        self.state = s;
192        Ok(())
193    }
194}