org_rust_exporter/
html.rs

1//! HTML Converter
2//!
3//! Converts the Org AST to its HTML representation.
4
5use 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
29/// Directly convert these types when used in special blocks
30/// to named blocks, e.g.:
31///
32/// #+begin_aside
33/// #+end_aside
34///
35/// becomes
36///
37/// <aside></aside>
38static 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
59/// HTML Content Exporter
60pub struct Html<'buf> {
61    buf: &'buf mut dyn fmt::Write,
62    // used footnotes
63    footnotes: Vec<NodeID>,
64    footnote_ids: HashMap<NodeID, usize>,
65    conf: ConfigOptions,
66    errors: Vec<ExportError>,
67}
68
69/// Wrapper around strings that need to be properly HTML escaped.
70pub(crate) struct HtmlEscape<S: AsRef<str>>(pub S);
71
72// TODO this is not appropriate for certain things (can break). i can't rememmber them atm
73// but you need to escape  more for certain stuff, it would be easier to just not use two separate htmlescapes
74// REVIEW: jetscii
75impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
76    // design based on:
77    // https://lise-henry.github.io/articles/optimising_strings.html
78    // we can iterate over bytes since it's not possible for
79    // an ascii character to appear in the codepoint of another larger char
80    // if we see an ascii, then it's guaranteed to be valid
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        let mut prev_pos = 0;
83        // there are other characters we could escape, but memchr caps out at 3
84        // the really important one is `<`, and then also probably &
85        // throwing in `>` for good measure
86        // based on:
87        // https://mina86.com/2021/no-you-dont-need-to-escape-that/
88        // there are invariants in the parsing (i hope) that should make
89        // using memchr3 okay. if not, consider using jetscii for more byte blasting
90
91        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"&lt;")?,
99                b'>' => write!(f, r"&gt;")?,
100                b'&' => write!(f, r"&amp;")?,
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                    // Greater Blocks
259                    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                        // html5 names are directly converted into tags
299                        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                    // Lesser blocks
320                    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                        // FIXME: apparently verse blocks contain objects...
378                        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                        // see if the link is present in someone's target
394                        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 we confirmed it's not a target, just interpret the string directly
401                        //
402                        // handles the [[./hello]] case for us.
403                        // turning it into <href="./hello">
404                        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                    // start writing alt (if there are children)
414                    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                // w!(self, "<span class=underline>")?;
496                // for id in &inner.0 {
497                //     self.export_rec(id, parser);
498                // }
499                // w!(self, "</span>")?;
500            }
501            Expr::BlankLine => {
502                // w!(self, "\n")?;
503            }
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                // if let Some(args) = inner.headers {
533                //     w!(self, "[{args}]")?;
534                // }
535                // w!(self, "{{{}}}", inner.body)?;
536            }
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                    //     .map_err(|e| ExportError::LogicError {
552                    //     span: node.start..node.end,
553                    //     source: LogicErrorKind::Include(e),
554                    // })?;
555                    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                // TODO/FIXME: this should be an error
568                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                    // TODO/FIXME: this should be an error
582                    w!(
583                        self,
584                        "{}",
585                        &latex_to_mathml(&pot_cont, DisplayStyle::Inline).unwrap(),
586                    );
587                }
588                LatexFragment::Display(inner) => {
589                    // TODO/FIXME: this should be an error
590                    w!(
591                        self,
592                        "{}\n",
593                        &latex_to_mathml(inner, DisplayStyle::Block).unwrap()
594                    );
595                }
596                LatexFragment::Inline(inner) => {
597                    // TODO/FIXME: this should be an error
598                    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 => { /*skip*/ }
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(), // must exist
746                    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                            // TODO alert for errors handled within macro
768                        }
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                    // NOTE: table uses <caption>. images use <figcaption>.
789                    // don't want to add complexity to the type to handle these,
790                    // so let the parents handle it
791                    self.export_rec(contents, parser);
792                }
793                Affiliated::Attr { .. } => {}
794            },
795            Expr::MacroDef(_) => {}
796            Expr::FootnoteDef(_) => {
797                // handled after root
798            }
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                // FIXME/REVIEW:
816                // 1. why does this exist
817                // 2. if this clause is activated then
818                // it's not properly handled when writing out footnote defs
819                // 3. when does this proc
820                // 4. sigh
821                //
822                // prevent duplicate ids:
823                // node ids are guaranteed to be unique
824                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
854// Writers for generic attributes
855impl<'buf> Html<'buf> {
856    /// Adds a property
857    fn prop(&mut self, node: &Node) {
858        // if the target needs an id
859        if let Some(tag_contents) = node.id_target.as_ref() {
860            w!(self, r#" id="{tag_contents}""#);
861        }
862
863        // attach any keys that need to be placed
864        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        // get last heading, and check if its title is Footnotes,
885        // if so, destroy it
886        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        // FIXME
924        // lifetime shenanigans making me do this.. can't figure em out
925        // would like to self.footnotes.iter(), but we get multiple
926        // immutable borrows, so self.footnotes.copied.iter(), but still no go
927        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        // just codifying what the output is here, not supposed to be set in stone
1120        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        // just codifying what the output is here, not supposed to be set in stone
1161        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        // tests dupes too
1191        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        // REVIEW; investigate different nodeids with export_buf and export
1209        // had to change 1.7 to 1.8 to pass the test
1210        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}