cmark_hamlet/
adapter.rs

1use std::borrow::Cow;
2
3use cmark::Tag as CmTag;
4use cmark::Event as CmEvent;
5use hamlet::Token as HmToken;
6
7/// _The_ adapter! An iterator that generates `hamlet::Token`s.
8pub struct Adapter<'a, I> {
9    cm_iter: I,
10    cm_looka: Option<CmEvent<'a>>, // for lookahead
11    hm_queue: Vec<HmToken<'a>>,
12    group_text: bool,
13    table_head: bool,
14}
15
16impl<'a, I> Adapter<'a, I>
17    where I: Iterator<Item = CmEvent<'a>>
18{
19    /// Create a `Token` iterator from an `Event` iterable. See [Text
20    /// handling](index.html#text-handling) for more details.
21    pub fn new<T>(iterable: T, group_text: bool) -> Adapter<'a, I>
22        where T: IntoIterator<IntoIter = I, Item = CmEvent<'a>> + 'a
23    {
24        Adapter {
25            cm_iter: iterable.into_iter(),
26            cm_looka: None,
27            hm_queue: Vec::with_capacity(2),
28            group_text: group_text,
29            table_head: false,
30        }
31    }
32
33    fn cm_start_tag(&mut self, tag: CmTag<'a>) -> HmToken<'a> {
34        match tag {
35            CmTag::BlockQuote |
36            CmTag::Code |
37            CmTag::Emphasis |
38            CmTag::Header(_) |
39            CmTag::Item |
40            CmTag::List(None) |
41            CmTag::List(Some(1)) |
42            CmTag::Paragraph |
43            CmTag::Strong |
44            CmTag::Table(_) |
45            CmTag::TableCell |
46            CmTag::TableRow => HmToken::start_tag(self.tag_map(tag), attrs!()),
47            CmTag::CodeBlock(lang) => {
48                self.hm_queue.push(HmToken::start_tag("code", attrs!()));
49                if lang.is_empty() {
50                    HmToken::start_tag("pre", attrs!())
51                } else {
52                    HmToken::start_tag("pre", attrs!(dataLang = lang))
53                }
54            }
55            CmTag::FootnoteDefinition(_) => unimplemented!(),
56            CmTag::Image(src, title) => {
57                let mut alt = String::from("");
58                while let Some(cm_ev) = self.cm_iter.next() {
59                    match cm_ev {
60                        CmEvent::Text(text) => alt.push_str(text.as_ref()),
61                        CmEvent::End(CmTag::Image(_, _)) => break,
62                        CmEvent::Start(CmTag::Image(_, _)) => unreachable!(),
63                        _ => (), // ignore other events
64                    }
65                }
66                let mut attrs = attrs!(src = src);
67                if !alt.is_empty() {
68                    attrs.set("alt", alt);
69                }
70                if !title.is_empty() {
71                    attrs.set("title", title);
72                }
73                HmToken::start_tag("img", attrs).closed()
74            }
75            CmTag::Link(href, title) => {
76                let mut attrs = attrs!(href = href);
77                if !title.is_empty() {
78                    attrs.set("title", title);
79                }
80                HmToken::start_tag("a", attrs)
81            }
82            CmTag::List(Some(start)) => {
83                HmToken::start_tag("ol", attrs!(start = format!("{}", start)))
84            }
85            CmTag::Rule => {
86                let _ = self.cm_iter.next(); // skip End(Rule)
87                HmToken::start_tag("hr", attrs!()).closed()
88            }
89            CmTag::TableHead => {
90                self.table_head = true;
91                HmToken::start_tag("tr", attrs!())
92            }
93        }
94    }
95
96    fn cm_end_tag(&mut self, tag: CmTag<'a>) -> HmToken<'a> {
97        match tag {
98            CmTag::Rule => unreachable!(),
99            CmTag::BlockQuote |
100            CmTag::Code |
101            CmTag::Emphasis |
102            CmTag::Header(_) |
103            CmTag::Item |
104            CmTag::List(_) |
105            CmTag::Paragraph |
106            CmTag::Strong |
107            CmTag::Table(_) |
108            CmTag::TableCell |
109            CmTag::TableRow => HmToken::end_tag(self.tag_map(tag)),
110            CmTag::CodeBlock(_) => {
111                self.hm_queue.push(HmToken::end_tag("pre"));
112                HmToken::end_tag("code")
113            }
114            CmTag::FootnoteDefinition(_) => unimplemented!(),
115            CmTag::Image(_, _) => unreachable!(),
116            CmTag::Link(_, _) => HmToken::end_tag("a"),
117            CmTag::TableHead => {
118                self.table_head = false;
119                HmToken::end_tag("tr")
120            }
121        }
122    }
123
124    fn cm_text(&mut self, mut s: Cow<'a, str>) -> HmToken<'a> {
125        if self.group_text {
126            while let Some(cm_ev) = self.cm_iter.next() {
127                match cm_ev {
128                    CmEvent::Text(text) => s.to_mut().push_str(text.as_ref()),
129                    CmEvent::SoftBreak => s.to_mut().push_str("\n"),
130                    _ => {
131                        self.cm_looka = Some(cm_ev);
132                        break;
133                    }
134                }
135            }
136        }
137        HmToken::Text(s)
138    }
139
140    fn tag_map(&self, tag: CmTag<'a>) -> Cow<'a, str> {
141        match tag {
142            CmTag::BlockQuote => "blockquote".into(),
143            CmTag::Code => "code".into(),
144            CmTag::Emphasis => "em".into(),
145            CmTag::Header(level) => format!("h{}", level).into(),
146            CmTag::Item => "li".into(),
147            CmTag::List(None) => "ul".into(),
148            CmTag::List(Some(_)) => "ol".into(),
149            CmTag::Paragraph => "p".into(),
150            CmTag::Rule => "hr".into(),
151            CmTag::Strong => "strong".into(),
152            CmTag::Table(_) => "table".into(),
153            CmTag::TableCell => {
154                if self.table_head {
155                    "th".into()
156                } else {
157                    "td".into()
158                }
159            }
160            CmTag::TableRow => "tr".into(),
161            _ => unreachable!(),
162        }
163    }
164}
165
166impl<'a, I> Iterator for Adapter<'a, I>
167    where I: Iterator<Item = CmEvent<'a>>
168{
169    type Item = HmToken<'a>;
170    fn next(&mut self) -> Option<Self::Item> {
171        if !self.hm_queue.is_empty() {
172            Some(self.hm_queue.remove(0))
173        } else {
174            let cm_ev = if let Some(cm_ev) = self.cm_looka.take() {
175                cm_ev
176            } else if let Some(cm_ev) = self.cm_iter.next() {
177                cm_ev
178            } else {
179                return None;
180            };
181            let hm_ev = match cm_ev {
182                CmEvent::Start(tag) => self.cm_start_tag(tag),
183                CmEvent::End(tag) => self.cm_end_tag(tag),
184                CmEvent::Text(text) => self.cm_text(text),
185                CmEvent::Html(html) | CmEvent::InlineHtml(html) => HmToken::RawText(html),
186                CmEvent::SoftBreak => self.cm_text("\n".into()),
187                CmEvent::HardBreak => HmToken::start_tag("br", attrs!()).closed(),
188                CmEvent::FootnoteReference(_) => unimplemented!(),
189            };
190            Some(hm_ev)
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use hamlet::Token as HmToken;
198    use cmark::Event as CmEvent;
199    use cmark::Parser;
200    use Adapter;
201    use std::borrow::Cow;
202
203    fn html_skel_map<'a, I>(ada: Adapter<'a, I>) -> Vec<Cow<'a, str>>
204        where I: Iterator<Item = CmEvent<'a>>
205    {
206        ada.map(|hm_ev| {
207               match hm_ev {
208                   HmToken::StartTag{name, ..} | HmToken::EndTag{name} => name,
209                   HmToken::Text(text) => text,
210                   _ => panic!("Bad token {:?}", hm_ev),
211               }
212           })
213           .collect()
214    }
215
216    #[test]
217    fn text_grouping() {
218        let md = "Multi\nLine\nText";
219        let ada = Adapter::new(Parser::new(md), true);
220        let res_vec = html_skel_map(ada);
221        assert_eq!(&["p", md, "p"], &*res_vec);
222    }
223
224    #[test]
225    fn code_grouping() {
226        let code = "Multi\n\nline[2][3]\n\ncode\n\n";
227        let md = String::from("```\n") + code + "```";
228        let ada = Adapter::new(Parser::new(&*md), true);
229        let res_vec = html_skel_map(ada);
230        assert_eq!(&["pre", "code", code, "code", "pre"], &*res_vec);
231    }
232}