1pub mod meta;
28pub mod node;
29mod parser;
30mod render;
31pub mod token;
32pub mod utils;
33
34use meta::Meta;
35use node::Node;
36use parser::Parser;
37
38#[derive(Debug)]
40pub struct PageOptions {}
41
42pub struct Page {
44 pub meta: Option<Meta>,
46 pub ast: Node,
48 pub content: String,
51 pub options: Option<PageOptions>,
53}
54
55impl Page {
56 pub fn new<S: AsRef<str>>(content: S) -> Self {
58 let (meta, ast, content) = Parser::new(content).parse();
59 Self {
60 meta,
61 ast,
62 content,
63 options: None,
64 }
65 }
66
67 pub fn with_options(mut self, options: PageOptions) -> Self {
68 self.options = Some(options);
69 self
70 }
71
72 pub fn render(&self) -> String {
86 self.render_with_hook(&|_| None)
87 }
88
89 pub fn render_latex(&self) -> String {
165 let mut page = include_str!("../assets/setup.tex").to_owned();
166 let mut document = render::latex::Cmd::new("document").enclosed();
167 if let Some(meta) = &self.meta {
168 let title =
169 render::latex::Cmd::new("title").with_posarg(&meta.title);
170 document.append_cmd(&title);
171 if let Some(authors) = &meta.authors {
172 let authors = render::latex::Cmd::new("author")
173 .with_posarg(authors.join(", "));
174 document.append_cmd(&authors);
175 }
176 let date = render::latex::Cmd::new("date")
177 .with_posarg(meta.date.to_string());
178 document.append_cmd(&date);
179 let maketitle = render::latex::Cmd::new("maketitle");
180 document.append_cmd(&maketitle);
181 }
182 document
183 .append(render::latex::generate(&self.ast, self.content.as_str()));
184 page.push_str(&document.to_string());
185 page
186 }
187
188 pub fn render_with_hook<F>(&self, hook: &F) -> String
193 where
194 F: Fn(&Node) -> Option<String>,
195 {
196 render::html::generate(&self.ast, self.content.as_str(), Some(hook))
197 }
198
199 pub fn transform<F, E>(&self, hook: F)
230 where
231 F: Fn(&Node) -> Result<(), E>,
232 {
233 self.ast.transform::<F, E>(&hook)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use html5ever::{
240 driver::ParseOpts, local_name, namespace_url, ns, parse_fragment,
241 tendril::TendrilSink, tree_builder::TreeSink, QualName,
242 };
243 use indoc::indoc;
244 use markup5ever_rcdom::{Handle, NodeData, RcDom};
245 use node::NodeTagName;
246
247 use crate::*;
248
249 fn is_self_closing_tag(tag: &str) -> bool {
250 let self_closing_tag_list = vec![
251 "circle", "ellipse", "line", "path", "polygon", "polyline", "rect",
253 "stop", "use", "area", "base", "br", "col", "command", "embed", "hr", "img",
255 "input", "keygen", "link", "meta", "param", "source", "track",
256 "wbr",
257 ];
258 self_closing_tag_list.iter().any(|&i| i == tag)
259 }
260
261 fn get_html_outline(dirty_html: &str) -> String {
262 fn walker(indent: usize, node: &Handle) -> String {
263 let indentstr = " ".repeat(indent);
264 let mut outline = indentstr.to_string();
265 if let NodeData::Element { ref name, .. } = node.data {
266 if is_self_closing_tag(&name.local) {
267 outline += &format!("<{}", name.local);
268 } else {
269 outline += &format!("<{}>\n", name.local);
270 }
271 }
272
273 for child in node.children.borrow().iter() {
274 if let NodeData::Element { .. } = child.data {
275 outline += &walker(indent + 2, child);
276 }
277 }
278
279 if let NodeData::Element { ref name, .. } = node.data {
280 if is_self_closing_tag(&name.local) {
281 outline += "/>\n";
282 } else {
283 outline += &format!("{}</{}>\n", indentstr, name.local);
284 }
285 }
286
287 outline
288 }
289
290 let parser = parse_fragment(
291 RcDom::default(),
292 ParseOpts::default(),
293 QualName::new(None, ns!(html), local_name!("body")),
294 vec![],
295 );
296 let mut dom = parser.one(dirty_html);
297 let html = dom.get_document();
298 let body = &html.children.borrow()[0];
299 let mut outline = String::new();
300 for child in body.children.borrow().iter() {
301 outline += &walker(0, child);
302 }
303 outline
304 }
305
306 #[test]
307 fn test_heading() {
308 let tcases = [
309 ("# title", "1"),
310 ("## title", "2"),
311 ("### title", "3"),
312 ("#### title", "4"),
313 ("##### title", "5"),
314 ("###### title", "6"),
315 ("####### title", "6"),
316 ];
317 for (content, level) in tcases {
318 let page = Page::new(content);
319 let ast = page.ast.data.borrow();
320 assert_eq!(ast.tag.name, NodeTagName::Section);
321 assert_eq!(ast.children[0].borrow().tag.name, NodeTagName::Heading);
322 assert_eq!(
323 ast.children[0]
324 .borrow()
325 .tag
326 .attrs
327 .get("level")
328 .map(|s| s.as_str()),
329 Some(level)
330 );
331 }
332 }
333
334 #[test]
335 fn test_list() {
336 let content = indoc! {r#"
337 - [nvim](https://neovim.io/) >= 0.7.0
338
339 nvim is great!
340
341 - [rust](https://www.rust-lang.org/tools/install) >= 1.64
342 "#};
343
344 let page = Page::new(content);
345 let html = page.render();
346 let outline = get_html_outline(html.as_str());
347 assert_eq!(
348 outline,
349 indoc! {r#"
350 <div>
351 <ul>
352 <li>
353 <a>
354 </a>
355 <p>
356 </p>
357 </li>
358 <li>
359 <a>
360 </a>
361 </li>
362 </ul>
363 </div>
364 "#}
365 );
366 }
367
368 #[test]
369 fn test_meta() {
370 let meta = r#"
371<!---
372title = "title"
373subtitle = "subtitle"
374date = "2023-08-27 10:39:05"
375authors = ["example <example@gmail>"]
376tags = []
377-->
378
379example
380
381"#;
382 let page = Page::new(meta);
383 assert!(page.meta.is_some());
384 let meta = page.meta.clone().unwrap();
385 assert_eq!(meta.title, "title");
386 assert_eq!(meta.subtitle, Some("subtitle".to_owned()));
387 assert_eq!(
388 format!("{}", meta.date.format("%Y-%m-%d %H:%M:%S")),
389 "2023-08-27 10:39:05"
390 );
391 }
392
393 #[test]
394 fn test_emphasis() {
395 let content = indoc! {r#"
396 This is a sentence with emphasis *itaclics* and **bold**.
397 "#};
398 let page = Page::new(content);
399 let html = page.render();
400 let wanted_html = indoc! {r#"
401 <div><p>This is a sentence with emphasis <em> itaclics </em>and <strong> bold </strong>. </p></div>
402 "#};
403 assert_eq!(html, wanted_html.trim());
404
405 let content = include_str!("../testdata/emphasis_01.md");
406 let page = Page::new(content);
407 let html = page.render();
408 assert_eq!(html, include_str!("../testdata/emphasis_01.html").trim());
409 }
410 #[test]
411 fn test_backquote_00() {
412 let content = include_str!("../testdata/backquote_00.md");
413 let page = Page::new(content);
414 let html = page.render();
415 let wanted_html = "<div><blockquote><p>a simple blockquote with very long body really long body ... </p></blockquote></div>";
416 assert_eq!(html, wanted_html);
417 }
418
419 #[test]
420 fn test_backquote_01() {
421 let content = include_str!("../testdata/backquote_01.md");
422 let page = Page::new(content);
423 let html = page.render();
424 let wanted_html = "<div><ul><li>title <blockquote><p>a simple line <br/>abc <strong> line </strong> <em> line </em>test </p></blockquote></li></ul></div>";
425 assert_eq!(html, wanted_html);
426 }
427
428 #[test]
429 fn test_backquote_02() {
430 let content = include_str!("../testdata/backquote_02.md");
431 let wanted_html = "<div><blockquote><p>a simple line <br/>line test </p></blockquote></div>";
432 let page = Page::new(content);
433 let html = page.render();
434 assert_eq!(html, wanted_html.trim());
435 }
436
437 #[test]
438 fn test_backquote_rich() {
439 let content = indoc! {r#"
440 > a simple line
441 >
442 > abc **line**
443 > *line*
444 test
445 "#};
446 let wanted_html = indoc! {r#"
447 <div><blockquote><p>a simple line <br/>abc <strong> line </strong> <em> line </em>test </p></blockquote></div>
448 "#};
449 let page = Page::new(content);
450 let html = page.render();
451 assert_eq!(html, wanted_html.trim());
452 }
453
454 #[test]
455 fn test_backquote_unicode() {
456 let content = indoc! {r#"
457 这是摘要
458
459 >测试
460 >
461 > 再次测试
462 "#};
463 let wanted_html = indoc! {r#"
464 <div><p>这是摘要</p><blockquote><p>测试<br/>再次测试</p></blockquote></div>
465 "#};
466 let page = Page::new(content);
467 let html = page.render();
468 assert_eq!(html, wanted_html.trim());
469 }
470
471 #[test]
472 fn test_para_ending_whitesapce_00() {
473 let content = include_str!("../testdata/para_ending_whitespace_00.md");
475 let wanted_html = indoc! {r#"
476 <div><p>北京奥运会开幕式时间为 2008 年 8 月 8 日</p></div>
477 "#};
478 let page = Page::new(content);
479 let html = page.render();
480 assert_eq!(html, wanted_html.trim());
481 }
482
483 #[test]
484 fn test_para_ending_whitesapce_01() {
485 let content = include_str!("../testdata/para_ending_whitespace_01.md");
488 let wanted_html = indoc! {r#"
489 <div><p>这是一段长文本</p><blockquote><p>这是一段引用文本</p></blockquote></div>
490 "#};
491 let page = Page::new(content);
492 let html = page.render();
493 assert_eq!(html, wanted_html.trim());
494 }
495
496 #[test]
497 fn test_math_mode() {
498 let content = include_str!("../testdata/math_mode.md");
499 let page = Page::new(content);
500 let nodes = node::find_nodes_by_tag(&page.ast, node::NodeTagName::Math);
501 assert_eq!(nodes.len(), 2);
502 assert!(nodes[0].is_inlined(content));
503 assert!(!nodes[1].is_inlined(content));
504 }
505
506 #[test]
507 fn test_zh_cn_hybrid_in_para() {
508 let content = include_str!("../testdata/zh_cn_hybrid_in_para.md");
509 let page = Page::new(content);
510 let html = page.render();
511 assert_eq!(html, "<div><p>这是 2 根韭菜</p></div>");
512 }
513
514 #[test]
515 fn test_codeblock_00() {
516 let content = include_str!("../testdata/codeblock_00.md");
517 let page = Page::new(content);
518 let html = page.render();
519 assert_eq!(
520 html,
521 include_str!("../testdata/codeblock_00.html").trim_end()
522 );
523 }
524
525 #[test]
526 fn test_html_char_escape_00() {
527 let content = include_str!("../testdata/html_char_escape_00.md");
528 let page = Page::new(content);
529 let html = page.render();
530 assert_eq!(
531 html,
532 include_str!("../testdata/html_char_escape_00.html").trim_end()
533 );
534 }
535
536 #[test]
537 fn test_heading_00() {
538 let content = include_str!("../testdata/heading_00.md");
539 let page = Page::new(content);
540 let html = page.render();
541 assert_eq!(
542 html,
543 include_str!("../testdata/heading_00.html").trim_end()
544 );
545 }
546}