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, 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}
28// file types we can wrap an `img` around
29static IMAGE_TYPES: phf::Set<&str> = phf_set! {
30    "jpeg",
31    "jpg",
32    "png",
33    "gif",
34    "svg",
35    "webp",
36};
37
38/// Directly convert these types when used in special blocks
39/// to named blocks, e.g.:
40///
41/// #+begin_aside
42/// #+end_aside
43///
44/// becomes
45///
46/// <aside></aside>
47static 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
68/// HTML Content Exporter
69pub struct Html<'buf> {
70    buf: &'buf mut dyn fmt::Write,
71    // HACK: When we export a caption, insert the child id here to make sure
72    // it's not double exported
73    nox: HashSet<NodeID>, // no-export
74    // used footnotes
75    footnotes: Vec<NodeID>,
76    footnote_ids: HashMap<NodeID, usize>,
77    conf: ConfigOptions,
78    errors: Vec<ExportError>,
79}
80
81/// Wrapper around strings that need to be properly HTML escaped.
82pub(crate) struct HtmlEscape<S: AsRef<str>>(pub S);
83
84// TODO this is not appropriate for certain things (can break). i can't rememmber them atm
85// but you need to escape  more for certain stuff, it would be easier to just not use two separate htmlescapes
86// REVIEW: jetscii
87impl<'a, S: AsRef<str>> fmt::Display for HtmlEscape<S> {
88    // design based on:
89    // https://lise-henry.github.io/articles/optimising_strings.html
90    // we can iterate over bytes since it's not possible for
91    // an ascii character to appear in the codepoint of another larger char
92    // if we see an ascii, then it's guaranteed to be valid
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let mut prev_pos = 0;
95        // there are other characters we could escape, but memchr caps out at 3
96        // the really important one is `<`, and then also probably &
97        // throwing in `>` for good measure
98        // based on:
99        // https://mina86.com/2021/no-you-dont-need-to-escape-that/
100        // there are invariants in the parsing (i hope) that should make
101        // using memchr3 okay. if not, consider using jetscii for more byte blasting
102
103        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"&lt;")?,
111                b'>' => write!(f, r"&gt;")?,
112                b'&' => write!(f, r"&amp;")?,
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        // avoid parsing this node
242        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                    // Greater Blocks
276                    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                        // html5 names are directly converted into tags
316                        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                    // Lesser blocks
337                    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                        // FIXME: apparently verse blocks contain objects...
395                        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                        // see if the link is present in someone's target
411                        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 we confirmed it's not a target, just interpret the string directly
418                        //
419                        // handles the [[./hello]] case for us.
420                        // turning it into <href="./hello">
421                        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                                // HACK: we just want to jump outta here, everything else doesnt make sense
449                                // in an image context
450                                "".into()
451                            }
452                        };
453
454                        // extract extension_type
455                        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                                // start writing alt (if there are children)
462                                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                // w!(self, "<span class=underline>")?;
520                // for id in &inner.0 {
521                //     self.export_rec(id, parser);
522                // }
523                // w!(self, "</span>")?;
524            }
525            Expr::BlankLine => {
526                // w!(self, "\n")?;
527            }
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                // if let Some(args) = inner.headers {
557                //     w!(self, "[{args}]")?;
558                // }
559                // w!(self, "{{{}}}", inner.body)?;
560            }
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                    //     .map_err(|e| ExportError::LogicError {
576                    //     span: node.start..node.end,
577                    //     source: LogicErrorKind::Include(e),
578                    // })?;
579                    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                // TODO/FIXME: this should be an error
592                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                    // TODO/FIXME: this should be an error
606                    w!(
607                        self,
608                        "{}",
609                        &latex_to_mathml(&pot_cont, DisplayStyle::Inline).unwrap(),
610                    );
611                }
612                LatexFragment::Display(inner) => {
613                    // TODO/FIXME: this should be an error
614                    w!(
615                        self,
616                        "{}\n",
617                        &latex_to_mathml(inner, DisplayStyle::Block).unwrap()
618                    );
619                }
620                LatexFragment::Inline(inner) => {
621                    // TODO/FIXME: this should be an error
622                    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 => { /*skip*/ }
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(), // must exist
765                    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                            // TODO alert for errors handled within macro
787                        }
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                // handled after root
822            }
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                // FIXME/REVIEW:
840                // 1. why does this exist
841                // 2. if this clause is activated then
842                // it's not properly handled when writing out footnote defs
843                // 3. when does this proc
844                // 4. sigh
845                //
846                // prevent duplicate ids:
847                // node ids are guaranteed to be unique
848                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
878// Writers for generic attributes
879impl<'buf> Html<'buf> {
880    /// Adds a property
881    fn prop(&mut self, node: &Node) {
882        // if the target needs an id
883        if let Some(tag_contents) = node.id_target.as_ref() {
884            w!(self, r#" id="{tag_contents}""#);
885        }
886
887        // attach any keys that need to be placed
888        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        // get last heading, and check if its title is Footnotes,
909        // if so, destroy it
910        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        // FIXME
948        // lifetime shenanigans making me do this.. can't figure em out
949        // would like to self.footnotes.iter(), but we get multiple
950        // immutable borrows, so self.footnotes.copied.iter(), but still no go
951        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        // just codifying what the output is here, not supposed to be set in stone
1144        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        // just codifying what the output is here, not supposed to be set in stone
1185        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        // tests dupes too
1215        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        // REVIEW; investigate different nodeids with export_buf and export
1233        // had to change 1.7 to 1.8 to pass the test
1234        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}