context_notation/
html.rs

1use pest::{iterators::Pair, Parser};
2
3use crate::{
4    code::convert_code_to_html, safety::html::unsafe_string, ContextParser, ContextParserError,
5    Rule,
6};
7
8fn convert_inner_pair_to_html(pair: Pair<'_, Rule>) -> Option<String> {
9    pair.into_inner()
10        .next()
11        .and_then(|pair| convert_pair_to_html(pair).ok())
12        .and_then(|converted| converted)
13}
14
15fn convert_list_to_html(pair: Pair<'_, Rule>) -> Option<String> {
16    let is_nested = match pair.as_rule() {
17        Rule::List => false,
18        Rule::NestedList => true,
19        _ => unreachable!("Only list rules are handled here"),
20    };
21
22    let mut pairs = pair.into_inner();
23
24    let Some(indentation) = pairs
25        .next()
26        .and_then(|pair| convert_pair_to_html(pair).ok())
27        .and_then(|pair| pair)
28    else {
29        return None;
30    };
31
32    let items = pairs
33        .filter_map(|pair| convert_pair_to_html(pair).unwrap_or_default())
34        .map(|item| format!("<span>{indentation}{item}</span>"))
35        .collect::<Vec<String>>()
36        .join("&#10;");
37
38    let suffix = if is_nested { "" } else { "&#10;" };
39
40    Some(format!("<span class=\"list\">{}</span>{}", items, suffix))
41}
42
43fn convert_pair_to_html(pair: Pair<'_, Rule>) -> Result<Option<String>, ContextParserError> {
44    Ok(Some(match pair.as_rule() {
45        Rule::IgnoredUnicode | Rule::EOI => return Ok(None),
46
47        Rule::Block
48        | Rule::Span
49        | Rule::Text
50        | Rule::BlockContent
51        | Rule::MentionBoundary
52        | Rule::SlashLinkBoundary
53        | Rule::HashTagBoundary
54        | Rule::QuoteBoundary
55        | Rule::CodeBoundary
56        | Rule::InlineCodeBoundary
57        | Rule::ListItemBoundary
58        | Rule::WikiLinkOpenBoundary
59        | Rule::WikiLinkCloseBoundary
60        | Rule::LeadingInlineBoundary
61        | Rule::Bridge
62        | Rule::BridgeCharacter
63        | Rule::TextCharacter
64        | Rule::NonTextSpan
65        | Rule::Contiguous
66        | Rule::Terminator
67        | Rule::Whitespace
68        | Rule::Indentation
69        | Rule::SlugSpecialCharacter
70        | Rule::Protocol
71        | Rule::Context => unsafe_string(pair.as_str()).into(),
72
73        Rule::Empty => r#"<span class="empty">&#10;</span>"#.into(),
74        // Rule::Empty => r#"<span class="empty"></span><br>"#.into(),
75        // Rule::Empty => r#"<br>"#.into(),
76        Rule::Slug => format!(
77            "<span class=\"slug\">{}</span>",
78            unsafe_string(pair.as_str())
79        ),
80        Rule::Petname => format!(
81            "<span class=\"petname\">{}</span>",
82            unsafe_string(pair.as_str())
83        ),
84        Rule::HyperLink => format!(
85            "<span class=\"hyperlink\"><a href=\"{0}\" target=\"_blank\">{0}</a></span>",
86            unsafe_string(pair.as_str())
87        ),
88        Rule::Mention => {
89            let Some(petname) = convert_inner_pair_to_html(pair) else {
90                return Ok(None);
91            };
92
93            format!("<span class=\"mention\">@{}</span>", petname)
94        }
95        Rule::SlashLink => {
96            let mut pairs = pair.into_inner();
97            let mut mention = None;
98            let mut link = None;
99
100            while let Some(pair) = pairs.next() {
101                match pair.as_rule() {
102                    Rule::Mention => {
103                        mention = convert_pair_to_html(pair)?;
104                    }
105                    Rule::Slug => link = convert_pair_to_html(pair)?,
106                    _ => return Ok(None),
107                }
108            }
109
110            link.map(|link| {
111                format!(
112                    "<span class=\"slashlink\">{}/{}</span>",
113                    mention.unwrap_or_default(),
114                    link
115                )
116            })
117            .unwrap_or_default()
118        }
119        Rule::HashTag => {
120            let Some(slug) = convert_inner_pair_to_html(pair) else {
121                return Ok(None);
122            };
123
124            format!("<span class=\"hashtag\">#{}</span>", slug)
125        }
126        Rule::InlineCode => {
127            let Some(value) = convert_inner_pair_to_html(pair) else {
128                return Ok(None);
129            };
130
131            format!("<span class=\"inlinecode\"><span class=\"fence\">`</span>{}<span class=\"fence\">`</span></span>", value)
132        }
133        Rule::InlineCodeValue => {
134            format!(
135                "<span class=\"inlinecodevalue\">{}</span>",
136                unsafe_string(pair.as_str())
137            )
138        }
139        Rule::WikiLink => {
140            let Some(value) = convert_inner_pair_to_html(pair) else {
141                return Ok(None);
142            };
143
144            format!("<span class=\"wikilink\">[[{}]]</span>", value)
145        }
146        Rule::WikiLinkValue => {
147            format!(
148                "<span class=\"wikilinkvalue\">{}</span>",
149                unsafe_string(pair.as_str())
150            )
151        }
152        Rule::Paragraph => {
153            let content = pair
154                .into_inner()
155                .map(|pair| {
156                    convert_pair_to_html(pair)
157                        .unwrap_or_default()
158                        .unwrap_or_default()
159                })
160                .collect::<Vec<String>>()
161                .concat();
162
163            format!("<span class=\"paragraph\">{}&#10;</span>", content)
164        }
165        Rule::Code => {
166            let mut pairs = pair.into_inner();
167            let mut slug = None;
168            let mut kind = None;
169            let mut code_value = None;
170
171            while let Some(pair) = pairs.next() {
172                match pair.as_rule() {
173                    Rule::Slug => {
174                        kind = Some(pair.as_str().to_owned());
175                        slug = convert_pair_to_html(pair)?;
176                    }
177                    Rule::CodeValue => {
178                        code_value = if let Some(kind) = kind.as_ref() {
179                            let code = pair.as_str();
180                            Some(
181                                convert_code_to_html(kind, code)
182                                    .unwrap_or_else(|| unsafe_string(code).to_string()),
183                            )
184                        } else {
185                            Some(unsafe_string(pair.as_str()).to_string())
186                        };
187                    }
188                    _ => return Ok(None),
189                }
190            }
191
192            format!(
193                    "<span class=\"code\"><span class=\"fence\">```{}</span>&#10;{}<span class=\"fence\">```</span>&#10;</span>",
194                    slug.unwrap_or_default(),
195                    code_value
196                        .unwrap_or_default()
197                )
198        }
199        Rule::CodeValue => {
200            format!(
201                "<span class=\"codevalue\">{}</span>",
202                unsafe_string(pair.as_str())
203            )
204        }
205        Rule::List | Rule::NestedList => convert_list_to_html(pair).unwrap_or_default(),
206        Rule::ListIndentation => {
207            if pair.as_str().len() == 0 {
208                String::new()
209            } else {
210                format!(
211                    "<span class=\"listidentation\">{}</span>",
212                    pair.as_str()
213                        .chars()
214                        .map(|_| "&nbsp;")
215                        .collect::<Vec<&str>>()
216                        .concat()
217                )
218            }
219        }
220        Rule::ListItem => {
221            let mut pairs = pair.into_inner();
222
223            let Some(content) = pairs
224                .next()
225                .and_then(|pair| convert_pair_to_html(pair).ok())
226                .and_then(|pair| pair)
227            else {
228                return Ok(None);
229            };
230
231            let sublist = if let Some(pair) = pairs.next() {
232                convert_pair_to_html(pair)
233                    .ok()
234                    .and_then(|pair| pair)
235                    .map(|sublist| format!("&#10;{}", sublist))
236            } else {
237                None
238            };
239
240            format!(
241                "<span class=\"listitem\"><span class=\"listbullet\">- </span>{content}{}</span>",
242                sublist.unwrap_or_default()
243            )
244        }
245        Rule::ListItemContent => {
246            let content = pair
247                .into_inner()
248                .filter_map(|pair| convert_pair_to_html(pair).ok().unwrap_or_default())
249                .collect::<Vec<String>>()
250                .concat();
251            format!("<span class=\"listitemcontent\">{content}</span>")
252        }
253        Rule::Quote => {
254            let lines = pair
255                .into_inner()
256                .filter_map(|pair| convert_pair_to_html(pair).unwrap_or_default())
257                .collect::<Vec<String>>()
258                .concat();
259            // .join("<br>");
260
261            format!("<span class=\"quote\">{lines}</span>")
262        }
263        Rule::QuoteLine => {
264            // let content
265            let content = pair
266                .into_inner()
267                .filter_map(|pair| convert_pair_to_html(pair).ok().unwrap_or_default())
268                .collect::<Vec<String>>()
269                .concat();
270            format!("<span class=\"quoteline\">&gt; <span class=\"quotelinecontent\">{content}</span></span>&#10;")
271        }
272    }))
273}
274
275pub fn convert_block_to_html(value: &str) -> Result<String, ContextParserError> {
276    let mut root = ContextParser::parse(Rule::Block, value)
277        .map_err(|error| ContextParserError::ParseError(format!("{error}")))?;
278    if let Some(pair) = root.next() {
279        Ok(convert_pair_to_html(pair)?.unwrap_or_default())
280    } else {
281        Ok(String::new())
282    }
283}
284
285pub fn convert_document_to_html(value: &str) -> Result<Vec<String>, ContextParserError> {
286    let root = ContextParser::parse(Rule::Context, value)
287        .map_err(|error| ContextParserError::ParseError(format!("{error}")))?;
288    let mut html = Vec::<String>::new();
289
290    for context in root {
291        for block in context.into_inner() {
292            // println!("{:?}: {:#?}", block.as_rule(), block);
293            if let Some(html_chunk) = convert_pair_to_html(block)? {
294                // println!("HTML: {}", html_chunk);
295                html.push(html_chunk);
296            }
297        }
298    }
299
300    Ok(html)
301}
302
303#[cfg(test)]
304mod tests {
305    use crate::html::{convert_block_to_html, convert_document_to_html};
306
307    #[test]
308    fn it_converts_a_basic_paragraph_to_html() {
309        let html = convert_block_to_html("Hello, world!").unwrap();
310        assert_eq!(html, "<span class=\"paragraph\">Hello, world!&#10;</span>");
311    }
312
313    #[test]
314    fn it_converts_multiple_basic_paragraphs_to_html() {
315        let html = convert_document_to_html("Hello, \nworld!").unwrap();
316        assert_eq!(
317            html,
318            vec![
319                "<span class=\"paragraph\">Hello, &#10;</span>",
320                "<span class=\"paragraph\">world!&#10;</span>"
321            ]
322        )
323    }
324
325    #[test]
326    fn it_converts_a_complex_paragraph_to_html() {
327        let html = convert_block_to_html(
328            "Hello, world! This #paragraph contains /interesting/content. [[Cool Stuff]].",
329        )
330        .unwrap();
331        assert_eq!(
332                html,
333                "<span class=\"paragraph\">Hello, world! This <span class=\"hashtag\">#<span class=\"slug\">paragraph</span></span> contains <span class=\"slashlink\">/<span class=\"slug\">interesting&#x2F;content</span></span>. <span class=\"wikilink\">[[<span class=\"wikilinkvalue\">Cool Stuff</span>]]</span>.&#10;</span>"
334            );
335    }
336
337    #[test]
338    fn it_converts_a_basic_list_to_html() {
339        let html = convert_block_to_html(
340            r#"- foo
341- bar
342- baz"#,
343        )
344        .unwrap();
345        assert_eq!(
346                html,
347                "<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">foo</span></span></span>&#10;<span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">bar</span></span></span>&#10;<span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">baz</span></span></span></span>&#10;"
348            );
349    }
350
351    #[test]
352    fn it_converts_a_basic_nested_list_to_html() {
353        let html = convert_block_to_html(
354            r#"- foo
355  - bar
356- baz"#,
357        )
358        .unwrap();
359        assert_eq!(
360                html,
361                "<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">foo</span>&#10;<span class=\"list\"><span><span class=\"listidentation\">&nbsp;&nbsp;</span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">bar</span></span></span></span></span></span>&#10;<span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">baz</span></span></span></span>&#10;"
362            );
363    }
364
365    #[test]
366    fn it_converts_the_project_example_to_html() {
367        let html = convert_document_to_html(include_str!("../example.context")).unwrap();
368        println!("{:#?}", html);
369
370        assert_eq!(html, vec![
371            "<span class=\"paragraph\">This is a paragraph. It can contain <span class=\"slashlink\">/<span class=\"slug\">slashlinks</span></span>. It may contain <span class=\"mention\">@<span class=\"petname\">mentions</span></span> and so <span class=\"slashlink\"><span class=\"mention\">@<span class=\"petname\">may</span></span>/<span class=\"slug\">slash&#x2F;links</span></span>. It may also contain <span class=\"hashtag\">#<span class=\"slug\">hashtags</span></span>. It may also contain <span class=\"hyperlink\"><a href=\"https:&#x2F;&#x2F;hyper.links\" target=\"_blank\">https:&#x2F;&#x2F;hyper.links</a></span>. It may also contain <span class=\"wikilink\">[[<span class=\"wikilinkvalue\">wiki-style links</span>]]</span>. It may also contain <span class=\"inlinecode\">`<span class=\"inlinecodevalue\">code blocks</span>`</span>.&#10;</span>",
372            "<span class=\"empty\">&#10;</span>",
373            "<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">This is a list</span></span></span>&#10;<span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">It can have several items</span>&#10;<span class=\"list\"><span><span class=\"listidentation\">&nbsp;&nbsp;</span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">The items can have nested lists</span></span></span>&#10;<span><span class=\"listidentation\">&nbsp;&nbsp;</span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">With multiple items</span>&#10;<span class=\"list\"><span><span class=\"listidentation\">&nbsp;&nbsp;&nbsp;&nbsp;</span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">They can be nested as deep as you like</span></span></span></span></span></span></span></span></span>&#10;<span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">With <span class=\"hashtag\">#<span class=\"slug\">all</span></span> the <span class=\"slashlink\">/<span class=\"slug\">same</span></span> <span class=\"slashlink\"><span class=\"mention\">@<span class=\"petname\">content</span></span>/<span class=\"slug\">types</span></span> as <span class=\"hyperlink\"><a href=\"http:&#x2F;&#x2F;a.paragraph\" target=\"_blank\">http:&#x2F;&#x2F;a.paragraph</a></span></span></span></span></span>&#10;",
374            "<span class=\"empty\">&#10;</span>",
375            "<span class=\"quote\"><span class=\"quoteline\">&gt; <span class=\"quotelinecontent\">This is a quote block</span></span>&#10;<span class=\"quoteline\">&gt; <span class=\"quotelinecontent\">It may be spread across several lines</span></span>&#10;<span class=\"quoteline\">&gt; <span class=\"quotelinecontent\">and may include <span class=\"hashtag\">#<span class=\"slug\">the</span></span> <span class=\"mention\">@<span class=\"petname\">same</span></span> <span class=\"slashlink\"><span class=\"mention\">@<span class=\"petname\">kinds</span></span>/<span class=\"slug\">of</span></span> <span class=\"hyperlink\"><a href=\"https:&#x2F;&#x2F;content.as&#x2F;a?paragraph\" target=\"_blank\">https:&#x2F;&#x2F;content.as&#x2F;a?paragraph</a></span></span></span>&#10;</span>",
376            "<span class=\"empty\">&#10;</span>",
377            "<span class=\"code\"><span class=\"fence\">```</span>&#10;&#x2F;&#x2F; This is a code block\n&#x2F;&#x2F; It can contain arbitrary text\n&#x2F;&#x2F; spread across many lines\nprint(&quot;Hi!&quot;);\n<span class=\"fence\">```</span></span>",
378            "<span class=\"empty\">&#10;</span>",
379            "<span class=\"code\"><span class=\"fence\">```<span class=\"slug\">rust</span></span>&#10;<span class=\"comment\">&#x2F;&#x2F; A code block may be tagged</span>&#10;<span class=\"keyword\">fn</span> <span class=\"function\">main</span>() {&#10;  <span class=\"macro\">println!</span>(<span class=\"string\">&quot;Hi!&quot;</span>);&#10;}&#10;<span class=\"fence\">```</span></span>",
380        ]);
381    }
382}