1use core::fmt;
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::fmt::Write;
9
10use latex2mathml::{DisplayStyle, latex_to_mathml};
11use memchr::memchr3_iter;
12use org_parser::element::{Affiliated, Block, CheckBox, ListKind, TableRow};
13use org_parser::object::{LatexFragment, PathReg, PlainOrRec};
14use org_parser::{Expr, Node, NodeID, Parser, parse_macro_call, parse_org};
15
16use crate::ExportError;
17use crate::include::include_handle;
18use crate::org_macros::macro_handle;
19use crate::types::{ConfigOptions, Exporter, ExporterInner, LogicErrorKind};
20use crate::utils::{Options, TocItem, process_toc};
21use phf::phf_set;
22
23macro_rules! w {
24 ($dst:expr, $($arg:tt)*) => {
25 $dst.write_fmt(format_args!($($arg)*)).expect("writing to buffer during export failed")
26 };
27}
28
29static HTML5_TYPES: phf::Set<&str> = phf_set! {
39"article",
40"aside",
41"audio",
42"canvas",
43"details",
44"figcaption",
45"figure",
46"footer",
47"header",
48"menu",
49"meter",
50"nav",
51"output",
52"progress",
53"section",
54"summary",
55"video",
56"picture",
57};
58
59pub struct Html<'buf> {
61 buf: &'buf mut dyn fmt::Write,
62 footnotes: Vec<NodeID>,
64 footnote_ids: HashMap<NodeID, usize>,
65 conf: ConfigOptions,
66 errors: Vec<ExportError>,
67}
68
69pub(crate) struct HtmlEscape<S: AsRef<str>>(pub S);
71
72impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 let mut prev_pos = 0;
83 let v = self.0.as_ref();
92 let escape_bytes = memchr3_iter(b'<', b'&', b'>', v.as_bytes());
93
94 for ret in escape_bytes {
95 write!(f, "{}", &v[prev_pos..ret])?;
96
97 match v.as_bytes()[ret] {
98 b'<' => write!(f, r"<")?,
99 b'>' => write!(f, r">")?,
100 b'&' => write!(f, r"&")?,
101 _ => unreachable!(),
102 }
103 prev_pos = ret + 1;
104 }
105
106 write!(f, "{}", &v[prev_pos..])
107 }
108}
109
110impl<'buf> Exporter<'buf> for Html<'buf> {
111 fn export(input: &str, conf: ConfigOptions) -> core::result::Result<String, Vec<ExportError>> {
112 let mut buf = String::new();
113 Html::export_buf(input, &mut buf, conf)?;
114 Ok(buf)
115 }
116
117 fn export_buf<'inp, T: fmt::Write>(
118 input: &'inp str,
119 buf: &'buf mut T,
120 conf: ConfigOptions,
121 ) -> core::result::Result<(), Vec<ExportError>> {
122 let parsed: Parser<'_> = parse_org(input);
123 Html::export_tree(&parsed, buf, conf)
124 }
125
126 fn export_tree<'inp, T: fmt::Write>(
127 parsed: &Parser,
128 buf: &'buf mut T,
129 conf: ConfigOptions,
130 ) -> core::result::Result<(), Vec<ExportError>> {
131 let mut obj = Html {
132 buf,
133 footnotes: Vec::new(),
134 footnote_ids: HashMap::new(),
135 conf,
136 errors: Vec::new(),
137 };
138
139 if let Ok(opts) = Options::handle_opts(parsed)
140 && let Ok(tocs) = process_toc(parsed, &opts)
141 {
142 handle_toc(parsed, &mut obj, &tocs);
143 }
144
145 obj.export_rec(&parsed.pool.root_id(), parsed);
146 obj.exp_footnotes(parsed);
147
148 if obj.errors().is_empty() {
149 Ok(())
150 } else {
151 Err(obj.errors)
152 }
153 }
154}
155
156fn handle_toc<'a, T: fmt::Write + ExporterInner<'a>>(
157 parsed: &Parser,
158 writer: &mut T,
159 tocs: &Vec<TocItem>,
160) {
161 w!(
162 writer,
163 r#"<nav id="table-of-contents" role="doc-toc">
164<h2>Table Of Contents</h2>
165<div id="text-table-of-contents" role="doc-toc">
166"#
167 );
168 w!(writer, "<ul>");
169 for toc in tocs {
170 toc_rec(parsed, writer, toc, 1);
171 }
172 w!(writer, "</ul>");
173 w!(writer, r#"</div></nav>"#);
174}
175
176fn toc_rec<'a, T: fmt::Write + ExporterInner<'a>>(
177 parser: &Parser,
178 writer: &mut T,
179 parent: &TocItem,
180 curr_level: u8,
181) {
182 w!(writer, "<li>");
183 if curr_level < parent.level {
184 w!(writer, "<ul>");
185 toc_rec(parser, writer, parent, curr_level + 1);
186 w!(writer, "</ul>");
187 } else {
188 w!(writer, r#"<a href=#{}>"#, parent.target);
189 for id in parent.name {
190 writer.export_rec(id, parser);
191 }
192 w!(writer, "</a>");
193 if !parent.children.is_empty() {
194 w!(writer, "<ul>");
195 for child in &parent.children {
196 toc_rec(parser, writer, child, curr_level + 1);
197 }
198 w!(writer, "</ul>");
199 }
200 }
201 w!(writer, "</li>");
202}
203
204impl<'buf> ExporterInner<'buf> for Html<'buf> {
205 fn export_macro_buf<'inp, T: fmt::Write>(
206 input: &'inp str,
207 buf: &'buf mut T,
208 conf: ConfigOptions,
209 ) -> core::result::Result<(), Vec<ExportError>> {
210 let parsed = parse_macro_call(input);
211 let mut obj = Html {
212 buf,
213 footnotes: Vec::new(),
214 footnote_ids: HashMap::new(),
215 conf,
216 errors: Vec::new(),
217 };
218
219 obj.export_rec(&parsed.pool.root_id(), &parsed);
220 if obj.errors().is_empty() {
221 Ok(())
222 } else {
223 Err(obj.errors)
224 }
225 }
226
227 fn export_rec(&mut self, node_id: &NodeID, parser: &Parser) {
228 let node = &parser.pool[*node_id];
229 match &node.obj {
230 Expr::Root(inner) => {
231 for id in inner {
232 self.export_rec(id, parser);
233 }
234 }
235 Expr::Heading(inner) => {
236 let heading_number: u8 = inner.heading_level.into();
237
238 w!(self, "<h{heading_number}");
239 self.prop(node);
240 w!(self, ">");
241
242 if let Some(title) = &inner.title {
243 for id in &title.1 {
244 self.export_rec(id, parser);
245 }
246 }
247
248 w!(self, "</h{heading_number}>\n");
249
250 if let Some(children) = &inner.children {
251 for id in children {
252 self.export_rec(id, parser);
253 }
254 }
255 }
256 Expr::Block(inner) => {
257 match inner {
258 Block::Center {
260 parameters,
261 contents,
262 } => {
263 if parameters.get("exports").is_some_and(|&x| x == "none") {
264 return;
265 }
266 w!(self, "<div");
267 self.class("org-center");
268 self.prop(node);
269 w!(self, ">\n");
270 for id in contents {
271 self.export_rec(id, parser);
272 }
273 w!(self, "</div>\n");
274 }
275 Block::Quote {
276 parameters,
277 contents,
278 } => {
279 if parameters.get("exports").is_some_and(|&x| x == "none") {
280 return;
281 }
282 w!(self, "<blockquote");
283 self.prop(node);
284 w!(self, ">\n");
285 for id in contents {
286 self.export_rec(id, parser);
287 }
288 w!(self, "</blockquote>\n");
289 }
290 Block::Special {
291 parameters,
292 contents,
293 name,
294 } => {
295 if parameters.get("exports").is_some_and(|&x| x == "none") {
296 return;
297 }
298 if HTML5_TYPES.contains(name) {
300 w!(self, "<{name}");
301 self.prop(node);
302 w!(self, ">\n");
303 for id in contents {
304 self.export_rec(id, parser);
305 }
306 w!(self, "</{name}>");
307 } else {
308 w!(self, "<div");
309 self.prop(node);
310 self.class(name);
311 w!(self, ">\n");
312 for id in contents {
313 self.export_rec(id, parser);
314 }
315 w!(self, "</div>\n");
316 }
317 }
318
319 Block::Comment {
321 parameters,
322 contents,
323 } => {
324 if parameters.get("exports").is_some_and(|&x| x == "none") {
325 return;
326 }
327 w!(self, "<!--{contents}-->\n");
328 }
329 Block::Example {
330 parameters,
331 contents,
332 } => {
333 if parameters.get("exports").is_some_and(|&x| x == "none") {
334 return;
335 }
336 w!(self, "<pre");
337 self.class("example");
338 self.prop(node);
339 w!(self, ">\n{}</pre>\n", HtmlEscape(contents));
340 }
341 Block::Export {
342 backend,
343 parameters,
344 contents,
345 } => {
346 if parameters.get("exports").is_some_and(|&x| x == "none") {
347 return;
348 }
349 if backend.is_some_and(|x| x == Html::backend_name()) {
350 w!(self, "{contents}\n");
351 }
352 }
353 Block::Src {
354 language,
355 parameters,
356 contents,
357 } => {
358 if parameters.get("exports").is_some_and(|&x| x == "none") {
359 return;
360 }
361 w!(self, "<pre>");
362 w!(self, "<code");
363 self.class("src");
364 if let Some(lang) = language {
365 self.class(&format!("src-{}", lang));
366 }
367 self.prop(node);
368 w!(self, ">\n{}</pre></code>\n", HtmlEscape(contents));
369 }
370 Block::Verse {
371 parameters,
372 contents,
373 } => {
374 if parameters.get("exports").is_some_and(|&x| x == "none") {
375 return;
376 }
377 w!(self, "<p");
379 self.class("verse");
380 self.prop(node);
381 w!(self, ">\n{}</p>\n", HtmlEscape(contents));
382 }
383 }
384 }
385 Expr::RegularLink(inner) => {
386 let path_link: String = match &inner.path.obj {
387 PathReg::PlainLink(a) => a.into(),
388 PathReg::Id(a) => format!("#{a}"),
389 PathReg::CustomId(a) => format!("#{a}"),
390 PathReg::Coderef(_) => todo!(),
391 PathReg::Unspecified(a) => {
392 let mut rita = String::new();
393 for (match_targ, ret) in parser.targets.iter() {
395 if match_targ.starts_with(a.as_ref()) {
396 rita = format!("#{ret}");
397 break;
398 }
399 }
400 if rita.is_empty() { a.to_string() } else { rita }
405 }
406 PathReg::File(a) => format!("{a}"),
407 };
408
409 if inner.is_image(parser) {
410 w!(self, "<img");
411 self.prop(node);
412 w!(self, r#" src="{}""#, HtmlEscape(&path_link));
413 w!(self, r#" alt=""#);
415 if let Some(children) = &inner.description {
416 for id in children {
417 self.export_rec(id, parser);
418 }
419 } else {
420 let alt_text: Cow<str> =
421 if let Some(slashed) = path_link.split('/').next_back() {
422 slashed.into()
423 } else {
424 path_link.into()
425 };
426 w!(self, "{}", HtmlEscape(alt_text));
427 }
428 w!(self, r#"">"#)
429 } else {
430 w!(self, r#"<a href="{}">"#, HtmlEscape(&path_link));
431 if let Some(children) = &inner.description {
432 for id in children {
433 self.export_rec(id, parser);
434 }
435 } else {
436 w!(self, "{}", HtmlEscape(inner.path.to_str(parser.source)));
437 }
438 w!(self, "</a>");
439 }
440 }
441
442 Expr::Paragraph(inner) => {
443 if inner.is_image(parser)
444 && let Expr::RegularLink(link) = &parser.pool[inner.0[0]].obj
445 && inner.is_image(parser)
446 {
447 w!(self, "<figure>\n");
448 if let Some(affiliate) = link.caption {
449 w!(self, "<figcaption>\n");
450 self.export_rec(&affiliate, parser);
451 w!(self, "</figcaption>\n");
452 }
453 self.export_rec(&inner.0[0], parser);
454 w!(self, "\n</figure>\n");
455 return;
456 }
457
458 w!(self, "<p");
459 self.prop(node);
460 w!(self, ">");
461
462 for id in &inner.0 {
463 self.export_rec(id, parser);
464 }
465 w!(self, "</p>\n");
466 }
467
468 Expr::Italic(inner) => {
469 w!(self, "<em>");
470 for id in &inner.0 {
471 self.export_rec(id, parser);
472 }
473 w!(self, "</em>");
474 }
475 Expr::Bold(inner) => {
476 w!(self, "<b>");
477 for id in &inner.0 {
478 self.export_rec(id, parser);
479 }
480 w!(self, "</b>");
481 }
482 Expr::StrikeThrough(inner) => {
483 w!(self, "<del>");
484 for id in &inner.0 {
485 self.export_rec(id, parser);
486 }
487 w!(self, "</del>");
488 }
489 Expr::Underline(inner) => {
490 w!(self, "<u>");
491 for id in &inner.0 {
492 self.export_rec(id, parser);
493 }
494 w!(self, "</u>");
495 }
501 Expr::BlankLine => {
502 }
504 Expr::SoftBreak => {
505 w!(self, " ");
506 }
507 Expr::LineBreak => {
508 w!(self, "\n<br>\n");
509 }
510 Expr::HorizontalRule => {
511 w!(self, "\n<hr>\n");
512 }
513 Expr::Plain(inner) => {
514 w!(self, "{}", HtmlEscape(inner));
515 }
516 Expr::Verbatim(inner) => {
517 w!(self, "<code>{}</code>", HtmlEscape(inner.0));
518 }
519 Expr::Code(inner) => {
520 w!(self, "<code>{}</code>", HtmlEscape(inner.0));
521 }
522 Expr::Comment(inner) => {
523 w!(self, "<!--{}-->", inner.0);
524 }
525 Expr::InlineSrc(inner) => {
526 w!(
527 self,
528 "<code class={}>{}</code>",
529 inner.lang,
530 HtmlEscape(inner.body)
531 );
532 }
537 Expr::Keyword(inner) => {
538 if inner.key.eq_ignore_ascii_case("include") {
539 w!(self, r#"<div class="org-include""#);
540 self.prop(node);
541 w!(self, ">");
542
543 if let Err(e) = include_handle(inner.val, self) {
544 self.errors().push(ExportError::LogicError {
545 span: node.start..node.end,
546 source: LogicErrorKind::Include(e),
547 });
548 return;
549 }
550
551 w!(self, "</div>");
556 }
557 }
558 Expr::LatexEnv(inner) => {
559 let formatted = &format!(
560 r"\begin{{{0}}}
561{1}
562\end{{{0}}}
563",
564 inner.name, inner.contents
565 );
566 let ret = latex_to_mathml(formatted, DisplayStyle::Block);
567 w!(
569 self,
570 "{}\n",
571 if let Ok(val) = &ret { val } else { formatted }
572 );
573 }
574 Expr::LatexFragment(inner) => match inner {
575 LatexFragment::Command { name, contents } => {
576 let mut pot_cont = String::new();
577 w!(pot_cont, r#"{name}"#);
578 if let Some(command_cont) = contents {
579 w!(pot_cont, "{{{command_cont}}}");
580 }
581 w!(
583 self,
584 "{}",
585 &latex_to_mathml(&pot_cont, DisplayStyle::Inline).unwrap(),
586 );
587 }
588 LatexFragment::Display(inner) => {
589 w!(
591 self,
592 "{}\n",
593 &latex_to_mathml(inner, DisplayStyle::Block).unwrap()
594 );
595 }
596 LatexFragment::Inline(inner) => {
597 w!(
599 self,
600 "{}",
601 &latex_to_mathml(inner, DisplayStyle::Inline).unwrap()
602 );
603 }
604 },
605 Expr::Item(inner) => {
606 if let Some(tag) = inner.tag {
607 w!(self, "<dt>{}</dt>", HtmlEscape(tag));
608 w!(self, "<dd>");
609 for id in &inner.children {
610 self.export_rec(id, parser);
611 }
612 w!(self, "</dd>");
613 } else {
614 w!(self, "<li");
615
616 if let Some(counter) = inner.counter_set {
617 self.attr("value", counter);
618 }
619
620 if let Some(check) = &inner.check_box {
621 self.class(match check {
622 CheckBox::Intermediate => "trans",
623 CheckBox::Off => "off",
624 CheckBox::On => "on",
625 });
626 }
627
628 w!(self, ">");
629
630 for id in &inner.children {
631 self.export_rec(id, parser);
632 }
633
634 w!(self, "</li>\n");
635 }
636 }
637 Expr::PlainList(inner) => {
638 let (tag, desc) = match inner.kind {
639 ListKind::Unordered => ("ul", ""),
640 ListKind::Ordered(counter_kind) => match counter_kind {
641 org_parser::element::CounterKind::Letter(c) => {
642 if c.is_ascii_uppercase() {
643 ("ol", r#" type="A""#)
644 } else {
645 ("ol", r#" type="a""#)
646 }
647 }
648 org_parser::element::CounterKind::Number(_) => ("ol", r#" type="1""#),
649 },
650 ListKind::Descriptive => ("dd", ""),
651 };
652 w!(self, "<{tag}{desc}");
653 self.prop(node);
654 w!(self, ">\n");
655 for id in &inner.children {
656 self.export_rec(id, parser);
657 }
658 w!(self, "</{tag}>\n");
659 }
660 Expr::PlainLink(inner) => {
661 w!(
662 self,
663 "<a href={0}:{1}>{0}:{1}</a>",
664 inner.protocol,
665 inner.path
666 );
667 }
668 Expr::Entity(inner) => {
669 w!(self, "{}", inner.mapped_item);
670 }
671 Expr::Table(inner) => {
672 w!(self, "<table");
673 self.prop(node);
674 w!(self, ">\n");
675
676 if let Some(affiliate) = inner.caption {
677 w!(self, "<caption>\n");
678 self.export_rec(&affiliate, parser);
679 w!(self, "</caption>\n");
680 }
681 for id in &inner.children {
682 self.export_rec(id, parser);
683 }
684
685 w!(self, "</table>\n");
686 }
687
688 Expr::TableRow(inner) => {
689 match inner {
690 TableRow::Rule => { }
691 TableRow::Standard(stands) => {
692 w!(self, "<tr>\n");
693 for id in stands.iter() {
694 self.export_rec(id, parser);
695 }
696 w!(self, "</tr>\n");
697 }
698 }
699 }
700 Expr::TableCell(inner) => {
701 w!(self, "<td>");
702 for id in &inner.0 {
703 self.export_rec(id, parser);
704 }
705 w!(self, "</td>\n");
706 }
707 Expr::Emoji(inner) => {
708 w!(self, "{}", inner.mapped_item);
709 }
710 Expr::Superscript(inner) => {
711 w!(self, "<sup>");
712 match &inner.0 {
713 PlainOrRec::Plain(inner) => {
714 w!(self, "{inner}");
715 }
716 PlainOrRec::Rec(inner) => {
717 for id in inner {
718 self.export_rec(id, parser);
719 }
720 }
721 }
722 w!(self, "</sup>");
723 }
724 Expr::Subscript(inner) => {
725 w!(self, "<sub>");
726 match &inner.0 {
727 PlainOrRec::Plain(inner) => {
728 w!(self, "{inner}");
729 }
730 PlainOrRec::Rec(inner) => {
731 for id in inner {
732 self.export_rec(id, parser);
733 }
734 }
735 }
736 w!(self, "</sub>");
737 }
738 Expr::Target(inner) => {
739 w!(self, "<span");
740 self.prop(node);
741 w!(self, ">");
742 w!(
743 self,
744 "<span id={}>{}</span>",
745 parser.pool[*node_id].id_target.as_ref().unwrap(), HtmlEscape(inner.0)
747 );
748 }
749 Expr::Macro(macro_call) => {
750 let macro_contents = match macro_handle(parser, macro_call, self.config_opts()) {
751 Ok(contents) => contents,
752 Err(e) => {
753 self.errors().push(ExportError::LogicError {
754 span: node.start..node.end,
755 source: LogicErrorKind::Macro(e),
756 });
757 return;
758 }
759 };
760
761 match macro_contents {
762 Cow::Owned(p) => {
763 if let Err(mut err_vec) =
764 Html::export_macro_buf(&p, self, self.config_opts().clone())
765 {
766 self.errors().append(&mut err_vec);
767 }
769 }
770 Cow::Borrowed(r) => {
771 w!(self, "{}", HtmlEscape(r));
772 }
773 }
774 }
775 Expr::Drawer(inner) => {
776 for id in &inner.children {
777 self.export_rec(id, parser);
778 }
779 }
780 Expr::ExportSnippet(inner) => {
781 if inner.backend == Html::backend_name() {
782 w!(self, "{}", inner.contents);
783 }
784 }
785 Expr::Affiliated(inner) => match inner {
786 Affiliated::Name(_id) => {}
787 Affiliated::Caption(contents) => {
788 self.export_rec(contents, parser);
792 }
793 Affiliated::Attr { .. } => {}
794 },
795 Expr::MacroDef(_) => {}
796 Expr::FootnoteDef(_) => {
797 }
799 Expr::FootnoteRef(inner) => {
800 let foot_len = self.footnotes.len();
801 let target_id = if let Some(label) = inner.label {
802 if let Some(def_id) = parser.footnotes.get(label) {
803 *def_id
804 } else {
805 *node_id
806 }
807 } else {
808 *node_id
809 };
810
811 let index = *self.footnote_ids.entry(target_id).or_insert_with(|| {
812 self.footnotes.push(target_id);
813 foot_len + 1
814 });
815 let fn_id = if index != foot_len + 1 {
825 format!("{index}.{node_id}")
826 } else {
827 format!("{index}")
828 };
829
830 w!(
831 self,
832 r##"<sup>
833 <a id="fnr.{0}" href="#fn.{1}" class="footref" role="doc-backlink">{1}</a>
834</sup>"##,
835 fn_id,
836 index,
837 );
838 }
839 }
840 }
841
842 fn backend_name() -> &'static str {
843 "html"
844 }
845
846 fn config_opts(&self) -> &ConfigOptions {
847 &self.conf
848 }
849 fn errors(&mut self) -> &mut Vec<ExportError> {
850 &mut self.errors
851 }
852}
853
854impl<'buf> Html<'buf> {
856 fn prop(&mut self, node: &Node) {
858 if let Some(tag_contents) = node.id_target.as_ref() {
860 w!(self, r#" id="{tag_contents}""#);
861 }
862
863 if let Some(attrs) = node.attrs.get(Html::backend_name()) {
865 for (key, val) in attrs {
866 self.attr(key, val);
867 }
868 }
869 }
870
871 fn class(&mut self, name: &str) {
872 w!(self, r#" class="{name}""#);
873 }
874
875 fn attr(&mut self, key: &str, val: &str) {
876 w!(self, r#" {}="{}""#, key, HtmlEscape(val));
877 }
878
879 fn exp_footnotes(&mut self, parser: &Parser) {
880 if self.footnotes.is_empty() {
881 return;
882 }
883
884 let heading_query = parser.pool.iter().rev().find(|node| {
887 if let Expr::Heading(head) = &node.obj
888 && let Some(title) = &head.title
889 && title.0 == "Footnotes\n"
890 {
891 return true;
892 }
893
894 false
895 });
896
897 w!(
898 self,
899 r#"
900<div id="footnotes">
901 <style>
902 .footdef p {{
903 display:inline;
904 }}
905 </style>
906"#
907 );
908
909 if heading_query.is_none() {
910 w!(
911 self,
912 r#" <h2 class="footnotes">Footnotes</h2>
913"#
914 );
915 }
916
917 w!(
918 self,
919 r#" <div id="text-footnotes">
920"#
921 );
922
923 let man = self.footnotes.clone();
928 for (mut pos, def_id) in man.iter().enumerate() {
929 pos += 1;
930 w!(
931 self,
932 r##"
933
934<div class="footdef">
935<sup>
936 <a id="fn.{pos}" href= "#fnr.{pos}" role="doc-backlink">{pos}</a>
937</sup>
938"##
939 );
940 match &parser.pool[*def_id].obj {
941 Expr::FootnoteDef(fn_def) => {
942 for child_id in &fn_def.children {
943 self.export_rec(child_id, parser);
944 }
945 }
946 Expr::FootnoteRef(fn_ref) => {
947 if let Some(children) = fn_ref.children.as_ref() {
948 for child_id in children {
949 self.export_rec(child_id, parser);
950 }
951 }
952 }
953 _ => (),
954 }
955 w!(self, r#"</div>"#);
956 }
957 w!(self, "\n </div>\n</div>");
958 }
959}
960
961impl fmt::Write for Html<'_> {
962 fn write_str(&mut self, s: &str) -> fmt::Result {
963 self.buf.write_str(s)
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use pretty_assertions::assert_eq;
971
972 fn html_export(input: &str) -> String {
973 Html::export(input, ConfigOptions::default()).unwrap()
974 }
975 #[test]
976 fn combined_macros() {
977 let a = html_export(
978 r"#+macro: poem hiii $1 $2 text
979{{{poem(cool,three)}}}
980",
981 );
982
983 assert_eq!(
984 a,
985 r"<p>hiii cool three text</p>
986"
987 );
988 }
989
990 #[test]
991 fn keyword_macro() {
992 let a = html_export(
993 r"
994 #+title: hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
995{{{keyword(title)}}}
996",
997 );
998
999 assert_eq!(
1000 a,
1001 r"<p>hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii</p>
1002",
1003 );
1004 }
1005
1006 #[test]
1007 fn line_break() {
1008 let a = html_export(
1009 r" abc\\
1010",
1011 );
1012
1013 assert_eq!(
1014 a,
1015 r"<p>abc
1016<br>
1017</p>
1018",
1019 );
1020
1021 let n = html_export(
1022 r" abc\\ q
1023",
1024 );
1025
1026 assert_eq!(
1027 n,
1028 r"<p>abc\\ q</p>
1029",
1030 );
1031 }
1032
1033 #[test]
1034 fn horizontal_rule() {
1035 let a = html_export(
1036 r"-----
1037",
1038 );
1039
1040 let b = html_export(
1041 r" -----
1042",
1043 );
1044
1045 let c = html_export(
1046 r" -------------------------
1047",
1048 );
1049
1050 assert_eq!(a, b);
1051 assert_eq!(b, c);
1052 assert_eq!(a, c);
1053
1054 let nb = html_export(
1055 r" ----
1056",
1057 );
1058
1059 assert_eq!(
1060 nb,
1061 r"<p>----</p>
1062",
1063 );
1064 }
1065
1066 #[test]
1067 fn correct_cache() {
1068 let a = html_export(
1069 r"
1070- one
1071- two
1072
1073\begin{align}
1074abc &+ 10\\
1075\end{align}
1076",
1077 );
1078 println!("{a}");
1079 }
1080
1081 #[test]
1082 fn html_unicode() {
1083 let a = html_export(
1084 r"a é😳
1085",
1086 );
1087
1088 assert_eq!(
1089 a,
1090 r"<p>a é😳</p>
1091"
1092 );
1093 }
1094
1095 #[test]
1096 fn list_counter_set() {
1097 let a = html_export(
1098 r"
10991. [@4] wordsss??
1100",
1101 );
1102
1103 assert_eq!(
1104 a,
1105 r#"<ol type="1">
1106<li value="4"><p>wordsss??</p>
1107</li>
1108</ol>
1109"#,
1110 );
1111 }
1112 #[test]
1113 fn anon_footnote() {
1114 let a = html_export(
1115 r"
1116hi [fn:next:coolio] yeah [fn:next]
1117",
1118 );
1119 assert_eq!(
1121 a,
1122 r##"<p>hi <sup>
1123 <a id="fnr.1" href="#fn.1" class="footref" role="doc-backlink">1</a>
1124</sup> yeah <sup>
1125 <a id="fnr.1.6" href="#fn.1" class="footref" role="doc-backlink">1</a>
1126</sup></p>
1127
1128<div id="footnotes">
1129 <style>
1130 .footdef p {
1131 display:inline;
1132 }
1133 </style>
1134 <h2 class="footnotes">Footnotes</h2>
1135 <div id="text-footnotes">
1136
1137
1138<div class="footdef">
1139<sup>
1140 <a id="fn.1" href= "#fnr.1" role="doc-backlink">1</a>
1141</sup>
1142coolio</div>
1143 </div>
1144</div>"##
1145 );
1146 }
1147
1148 #[test]
1149 fn footnote_heading() {
1150 let a = html_export(
1151 r"
1152hello [fn:1]
1153
1154* Footnotes
1155
1156[fn:1] world
1157",
1158 );
1159
1160 assert_eq!(
1162 a,
1163 r##"<p>hello <sup>
1164 <a id="fnr.1" href="#fn.1" class="footref" role="doc-backlink">1</a>
1165</sup></p>
1166<h1 id="footnotes">Footnotes</h1>
1167
1168<div id="footnotes">
1169 <style>
1170 .footdef p {
1171 display:inline;
1172 }
1173 </style>
1174 <div id="text-footnotes">
1175
1176
1177<div class="footdef">
1178<sup>
1179 <a id="fn.1" href= "#fnr.1" role="doc-backlink">1</a>
1180</sup>
1181<p>world</p>
1182</div>
1183 </div>
1184</div>"##
1185 );
1186 }
1187
1188 #[test]
1189 fn footnote_order() {
1190 let a = html_export(
1192 r#"
1193hi [fn:dupe] cool test [fn:coolnote] [fn:dupe:inlinefootnote]
1194coolest [fn:1] again [fn:1]
1195
1196novel [fn:next:coolio]
1197
1198
1199** Footnotes
1200
1201[fn:1] hi
1202[fn:dupe] abcdef
1203[fn:coolnote] words babby
1204
1205"#,
1206 );
1207
1208 assert_eq!(
1211 a,
1212 r##"<p>hi <sup>
1213 <a id="fnr.1" href="#fn.1" class="footref" role="doc-backlink">1</a>
1214</sup> cool test <sup>
1215 <a id="fnr.2" href="#fn.2" class="footref" role="doc-backlink">2</a>
1216</sup> <sup>
1217 <a id="fnr.1.8" href="#fn.1" class="footref" role="doc-backlink">1</a>
1218</sup> coolest <sup>
1219 <a id="fnr.3" href="#fn.3" class="footref" role="doc-backlink">3</a>
1220</sup> again <sup>
1221 <a id="fnr.3.13" href="#fn.3" class="footref" role="doc-backlink">3</a>
1222</sup></p>
1223<p>novel <sup>
1224 <a id="fnr.4" href="#fn.4" class="footref" role="doc-backlink">4</a>
1225</sup></p>
1226<h2 id="footnotes">Footnotes</h2>
1227
1228<div id="footnotes">
1229 <style>
1230 .footdef p {
1231 display:inline;
1232 }
1233 </style>
1234 <div id="text-footnotes">
1235
1236
1237<div class="footdef">
1238<sup>
1239 <a id="fn.1" href= "#fnr.1" role="doc-backlink">1</a>
1240</sup>
1241<p>abcdef</p>
1242</div>
1243
1244<div class="footdef">
1245<sup>
1246 <a id="fn.2" href= "#fnr.2" role="doc-backlink">2</a>
1247</sup>
1248<p>words babby</p>
1249</div>
1250
1251<div class="footdef">
1252<sup>
1253 <a id="fn.3" href= "#fnr.3" role="doc-backlink">3</a>
1254</sup>
1255<p>hi</p>
1256</div>
1257
1258<div class="footdef">
1259<sup>
1260 <a id="fn.4" href= "#fnr.4" role="doc-backlink">4</a>
1261</sup>
1262coolio</div>
1263 </div>
1264</div>"##
1265 );
1266 }
1267
1268 #[test]
1269 fn esoteric_footnotes() {
1270 let a = html_export(
1271 r"
1272And anonymous ones [fn::mysterious]
1273
1274what [fn::]
1275
1276bad [fn:]
1277",
1278 );
1279
1280 assert_eq!(
1281 a,
1282 r##"<p>And anonymous ones <sup>
1283 <a id="fnr.1" href="#fn.1" class="footref" role="doc-backlink">1</a>
1284</sup></p>
1285<p>what <sup>
1286 <a id="fnr.2" href="#fn.2" class="footref" role="doc-backlink">2</a>
1287</sup></p>
1288<p>bad [fn:]</p>
1289
1290<div id="footnotes">
1291 <style>
1292 .footdef p {
1293 display:inline;
1294 }
1295 </style>
1296 <h2 class="footnotes">Footnotes</h2>
1297 <div id="text-footnotes">
1298
1299
1300<div class="footdef">
1301<sup>
1302 <a id="fn.1" href= "#fnr.1" role="doc-backlink">1</a>
1303</sup>
1304mysterious</div>
1305
1306<div class="footdef">
1307<sup>
1308 <a id="fn.2" href= "#fnr.2" role="doc-backlink">2</a>
1309</sup>
1310</div>
1311 </div>
1312</div>"##
1313 );
1314 }
1315
1316 #[test]
1317 fn file_link() {
1318 let a = html_export(r"[[file:html.org][hi]]");
1319
1320 assert_eq!(
1321 a,
1322 r#"<p><a href="html.org">hi</a></p>
1323"#
1324 );
1325 }
1326
1327 #[test]
1328 fn file_link_image() {
1329 let a = html_export(
1330 r"
1331[[file:bmc.jpg]]
1332",
1333 );
1334 assert_eq!(
1335 a,
1336 r#"<figure>
1337<img src="bmc.jpg" alt="bmc.jpg">
1338</figure>
1339"#
1340 );
1341 }
1342
1343 #[test]
1344 fn basic_link_image() {
1345 let a = html_export(
1346 r"
1347[[https://upload.wikimedia.org/wikipedia/commons/a/a6/Org-mode-unicorn.svg]]
1348",
1349 );
1350
1351 assert_eq!(
1352 a,
1353 r#"<figure>
1354<img src="https://upload.wikimedia.org/wikipedia/commons/a/a6/Org-mode-unicorn.svg" alt="Org-mode-unicorn.svg">
1355</figure>
1356"#
1357 );
1358 }
1359
1360 #[test]
1361 fn unspecified_link() {
1362 let a = html_export(r"[[./hello]]");
1363
1364 assert_eq!(
1365 a,
1366 r##"<p><a href="./hello">./hello</a></p>
1367"##
1368 );
1369 }
1370
1371 #[test]
1372 fn checkbox() {
1373 let a = html_export("- [X]\n");
1374
1375 assert_eq!(
1376 a,
1377 r#"<ul>
1378<li class="on"></li>
1379</ul>
1380"#
1381 );
1382
1383 let b = html_export("- [ ]\n");
1384
1385 assert_eq!(
1386 b,
1387 r#"<ul>
1388<li class="off"></li>
1389</ul>
1390"#
1391 );
1392
1393 let c = html_export("- [-]\n");
1394
1395 assert_eq!(
1396 c,
1397 r#"<ul>
1398<li class="trans"></li>
1399</ul>
1400"#
1401 );
1402 }
1403
1404 #[test]
1405 fn words_with_line_breaks() {
1406 let a = r#"
1407
1408#+kw: hi
1409
1410* yeah
1411hello
1412
1413{{{keyword(kw)}}}
1414
1415content
1416
1417here
1418"#;
1419 assert_eq!(
1420 html_export(a),
1421 "<h1 id=\"yeah\">yeah</h1>\n<p>hello</p>\n<p>hi</p>\n<p>content</p>\n<p>here</p>\n"
1422 );
1423 }
1424
1425 #[test]
1426 fn link_caption() {
1427 let a = r#"
1428#+caption: yes
1429[[suki.jpg]]
1430"#;
1431
1432 assert_eq!(
1433 html_export(a),
1434 r#"<figure>
1435<figcaption>
1436<p> yes</p>
1437</figcaption>
1438<img src="suki.jpg" alt="suki.jpg">
1439</figure>
1440"#
1441 )
1442 }
1443
1444 #[test]
1445 fn tabale_caption() {
1446 let a = r#"
1447#+caption: i am a table
1448|a|b|c
1449"#;
1450
1451 assert_eq!(
1452 html_export(a),
1453 r#"<table>
1454<caption>
1455<p> i am a table</p>
1456</caption>
1457<tr>
1458<td>a</td>
1459<td>b</td>
1460<td>c</td>
1461</tr>
1462</table>
1463"#
1464 )
1465 }
1466}