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(" ");
37
38 let suffix = if is_nested { "" } else { " " };
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"> </span>"#.into(),
74 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\">{} </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> {}<span class=\"fence\">```</span> </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(|_| " ")
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!(" {}", 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 format!("<span class=\"quote\">{lines}</span>")
262 }
263 Rule::QuoteLine => {
264 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\">> <span class=\"quotelinecontent\">{content}</span></span> ")
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 if let Some(html_chunk) = convert_pair_to_html(block)? {
294 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! </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, </span>",
320 "<span class=\"paragraph\">world! </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/content</span></span>. <span class=\"wikilink\">[[<span class=\"wikilinkvalue\">Cool Stuff</span>]]</span>. </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> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">bar</span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">baz</span></span></span></span> "
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> <span class=\"list\"><span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">bar</span></span></span></span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">baz</span></span></span></span> "
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/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://hyper.links\" target=\"_blank\">https://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>. </span>",
372 "<span class=\"empty\"> </span>",
373 "<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">This is a list</span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">It can have several items</span> <span class=\"list\"><span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">The items can have nested lists</span></span></span> <span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">With multiple items</span> <span class=\"list\"><span><span class=\"listidentation\"> </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> <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://a.paragraph\" target=\"_blank\">http://a.paragraph</a></span></span></span></span></span> ",
374 "<span class=\"empty\"> </span>",
375 "<span class=\"quote\"><span class=\"quoteline\">> <span class=\"quotelinecontent\">This is a quote block</span></span> <span class=\"quoteline\">> <span class=\"quotelinecontent\">It may be spread across several lines</span></span> <span class=\"quoteline\">> <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://content.as/a?paragraph\" target=\"_blank\">https://content.as/a?paragraph</a></span></span></span> </span>",
376 "<span class=\"empty\"> </span>",
377 "<span class=\"code\"><span class=\"fence\">```</span> // This is a code block\n// It can contain arbitrary text\n// spread across many lines\nprint("Hi!");\n<span class=\"fence\">```</span></span>",
378 "<span class=\"empty\"> </span>",
379 "<span class=\"code\"><span class=\"fence\">```<span class=\"slug\">rust</span></span> <span class=\"comment\">// A code block may be tagged</span> <span class=\"keyword\">fn</span> <span class=\"function\">main</span>() { <span class=\"macro\">println!</span>(<span class=\"string\">"Hi!"</span>); } <span class=\"fence\">```</span></span>",
380 ]);
381 }
382}