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