latex_to_html/
emit.rs

1use crate::analysis::*;
2use crate::ast::*;
3use crate::math_svg::*;
4use crate::util::*;
5use convert_case::{Case, Casing};
6use indoc::{indoc, writedoc};
7use itertools::Itertools;
8use std::fmt::{Display, Formatter, Result, Write};
9use std::fs;
10use std::io::Write as IoWrite;
11use std::path::Path;
12use std::ptr::addr_of;
13use std::write;
14
15fn display_math<'a>(analysis: &'a Analysis<'a>, math: &'a Math<'a>) -> impl 'a + Display {
16    let src = analysis.math_image_source.get(&addr_of!(*math)).unwrap();
17    let number = analysis.math_numbering.get(&addr_of!(*math));
18    DisplayFn(move |out: &mut Formatter| {
19        use Math::*;
20        match math {
21            Inline(_) => {
22                write!(out, r#"<img src="{src}" class="inline-math">"#)?;
23            }
24            Display { source: _, label } | Mathpar { source: _, label } => {
25                let id_attr = display_label_id_attr(*label);
26                writedoc! {out, r#"
27                    <div{id_attr} class="display-math-row">
28                "#}?;
29
30                if let Some(number) = number {
31                    writedoc! {out, r#"
32                        <span>{number}</span>
33                    "#}?;
34                }
35                writedoc! {out, r#"
36                    <img src="{src}">
37                "#}?;
38                if let Some(number) = number {
39                    writedoc! {out, r#"
40                            <span>{number}</span>
41                        "#}?;
42                }
43                writedoc! {out, r#"
44                </div>"#}?;
45            }
46        }
47        Ok(())
48    })
49}
50
51fn display_label_id_attr(label_value: Option<&str>) -> impl '_ + Display {
52    DisplayFn(move |out: &mut Formatter| {
53        let label_value = match label_value {
54            None => {
55                return Ok(());
56            }
57            Some(label_value) => display_label_value(label_value),
58        };
59        write!(out, r#" id="{label_value}""#)?;
60        Ok(())
61    })
62}
63
64fn display_paragraph_part<'a>(
65    analysis: &'a Analysis<'a>,
66    part: &'a ParagraphPart,
67) -> impl 'a + Display {
68    DisplayFn(move |out: &mut Formatter| {
69        use ParagraphPart::*;
70        match part {
71            InlineWhitespace(ws) => {
72                let has_newlines = ws.find('\n').is_some();
73                if has_newlines {
74                    write!(out, "\n")?;
75                } else if !ws.is_empty() {
76                    write!(out, " ")?;
77                }
78            }
79            TextToken(tok) => out.write_str(tok)?,
80            Math(math) => {
81                write!(out, "{}", display_math(analysis, math))?;
82            }
83            Ref(value) => {
84                let name = match analysis.ref_display_text.get(value) {
85                    None => "???",
86                    Some(name) => name.as_str(),
87                };
88                let value = display_label_value(value);
89                write!(out, "<a href=\"#{value}\">{name}</a>")?;
90            }
91            Cite { ids, text } => {
92                let links = ids.iter().copied().format_with(", ", |id, f| {
93                    let display_text = match analysis.cite_display_text.get(id) {
94                        None => "???",
95                        Some(name) => name.as_str(),
96                    };
97                    let id = display_cite_value(id);
98                    f(&format_args!("<a href=\"#{id}\">{display_text}</a>"))
99                });
100                write!(out, "[{links}")?;
101                if let Some(text) = text {
102                    write!(out, ", ")?;
103                    for part in text.iter() {
104                        write!(out, "{}", display_paragraph_part(analysis, part))?;
105                    }
106                }
107                write!(out, "]")?;
108            }
109            Emph(child_paragraph) => {
110                write!(out, "<em>")?;
111                for part in child_paragraph.iter() {
112                    write!(out, "{}", display_paragraph_part(analysis, part))?;
113                }
114                write!(out, "</em>")?;
115            }
116            Textbf(paragraph) => {
117                write!(out, "<strong>")?;
118                for part in paragraph.iter() {
119                    write!(out, "{}", display_paragraph_part(analysis, part))?;
120                }
121                write!(out, "</strong>")?;
122            }
123            Textit(paragraph) => {
124                write!(out, "<i>")?;
125                for part in paragraph.iter() {
126                    write!(out, "{}", display_paragraph_part(analysis, part))?;
127                }
128                write!(out, "</i>")?;
129            }
130            Qed => {}
131            Itemize(items) => {
132                write!(out, "<ul>\n")?;
133                for item in items {
134                    assert!(item.label.is_none());
135                    write!(out, "<li>\n")?;
136                    for paragraph in item.content.iter() {
137                        display_paragraph(analysis, paragraph).fmt(out)?;
138                    }
139                    write!(out, "</li>\n")?;
140                }
141                write!(out, "</ul>\n")?;
142            }
143            Enumerate(items) => {
144                write!(out, "<ol>\n")?;
145                for item in items {
146                    let id_attr = display_label_id_attr(item.label);
147                    write!(out, "<li{id_attr}>\n")?;
148                    for paragraph in item.content.iter() {
149                        display_paragraph(analysis, paragraph).fmt(out)?;
150                    }
151                    write!(out, "</li>\n")?;
152                }
153                write!(out, "</ol>\n")?;
154            }
155            Todo => (),
156            Footnote(_) => {
157                // TODO
158            }
159        }
160        Ok(())
161    })
162}
163
164fn display_paragraph<'a>(
165    analysis: &'a Analysis<'a>,
166    paragraph: &'a Paragraph,
167) -> impl 'a + Display {
168    DisplayFn(|out: &mut Formatter| {
169        writedoc! {out, r#"
170            <div class="paragraph">
171        "#}?;
172        for part in paragraph.iter() {
173            write!(out, "{}", display_paragraph_part(analysis, part))?;
174        }
175        writedoc! {out, r#"
176            </div>
177        "#}?;
178        Ok(())
179    })
180}
181
182pub fn display_head(title: impl Display) -> impl Display {
183    DisplayFn(move |out: &mut Formatter| {
184        writedoc! {out, r#"
185              <head>
186              <meta charset="utf-8">
187              <meta name="viewport" content="width=device-width, initial-scale=1" />
188              <title>{title}</title>
189              <link rel="stylesheet" type="text/css" href="https://cdn.rawgit.com/dreampulse/computer-modern-web-font/master/fonts.css">
190              <link rel="stylesheet" type="text/css" href="style.css">
191              <link rel="stylesheet" type="text/css" href="{SVG_OUT_DIR}/geometry.css">
192              </head>
193        "#}?;
194        Ok(())
195    })
196}
197
198fn display_label_value(label_value: &str) -> impl '_ + Display {
199    label_value.replace(":", "-").to_case(Case::Kebab)
200}
201
202fn display_cite_value(label_value: &str) -> impl '_ + Display {
203    label_value.replace(":", "-").to_case(Case::Kebab)
204}
205
206fn display_theorem_header<'a>(
207    analysis: &'a Analysis,
208    name: &'a Paragraph<'a>,
209    note: Option<&'a Paragraph<'a>>,
210    number: Option<&'a str>,
211) -> impl 'a + Display {
212    DisplayFn(move |out: &mut Formatter| {
213        write!(out, "<h4>")?;
214        for part in name.iter() {
215            write!(out, "{}", display_paragraph_part(analysis, part))?;
216        }
217        if let Some(number) = number {
218            write!(out, " {number}")?;
219        }
220        if let Some(note) = note {
221            // TODO: Should add style so that this span is not bold.
222            write!(out, r#" <span class="theorem-note">("#)?;
223            for part in note.iter() {
224                write!(out, "{}", display_paragraph_part(analysis, part))?;
225            }
226            write!(out, ")</span>")?;
227        }
228        write!(out, ".\n")?;
229
230        write!(out, "</h4>")?;
231        Ok(())
232    })
233}
234
235fn display_title<'a>(title: Option<&'a Paragraph<'a>>) -> impl 'a + Display {
236    DisplayFn(move |out: &mut Formatter| {
237        match title {
238            None => (),
239            Some(parag) => {
240                for part in parag {
241                    use ParagraphPart::*;
242                    match part {
243                        TextToken(tok) => {
244                            write!(out, "{tok}")?;
245                        }
246                        InlineWhitespace(ws) => {
247                            if ws.len() > 0 {
248                                write!(out, " ")?;
249                            }
250                        }
251                        Math(_)
252                        | Ref(_)
253                        | Emph(_)
254                        | Textbf(_)
255                        | Textit(_)
256                        | Qed
257                        | Enumerate(_)
258                        | Itemize(_)
259                        | Todo
260                        | Cite { .. }
261                        | Footnote(_) => {
262                            panic!("Invalid node in title");
263                        }
264                    }
265                }
266            }
267        }
268        Ok(())
269    })
270}
271
272fn display_bib_person<'a>(person: &'a BibPerson<'a>) -> impl 'a + Display {
273    DisplayFn(move |out: &mut Formatter| {
274        for first_name in person.first_names.iter() {
275            use FirstName::*;
276            match first_name {
277                Full(name) => {
278                    write!(out, "{name} ")?;
279                }
280                Abbreviation(abbr) => {
281                    write!(out, "{abbr}. ")?;
282                }
283            }
284        }
285        let last_name = person.last_name;
286        write!(out, "{last_name}")?;
287        Ok(())
288    })
289}
290
291fn display_bib_entry<'a>(entry: &'a BibEntry<'a>) -> impl 'a + Display {
292    let title = entry.title;
293    let authors = &entry.authors;
294
295    let id_attr_value = display_cite_value(entry.tag);
296
297    DisplayFn(move |out: &mut Formatter| {
298        writedoc! {out, r#"
299            <li id="{id_attr_value}">
300        "#}?;
301        match authors.as_deref() {
302            None | Some([]) => (),
303            Some([author]) => {
304                write!(out, " {}.", display_bib_person(author))?;
305            }
306            Some([init @ .., before_last, last]) => {
307                for author in init {
308                    write!(out, " {},", display_bib_person(author))?;
309                }
310                write!(out, " {}", display_bib_person(before_last))?;
311                write!(out, " and {}.", display_bib_person(last))?;
312            }
313        };
314        if let Some(title) = title {
315            write!(out, " {title}.")?;
316        }
317
318        // TODO: Only on of journal, booktitle or series should be present.
319        if let Some(journal) = entry.journal {
320            write!(out, " {journal}")?;
321        }
322        if let Some(booktitle) = entry.booktitle {
323            write!(out, " {booktitle}")?;
324        }
325        if let Some(series) = entry.series {
326            write!(out, " {series}")?;
327        }
328
329        let has_volume_or_number = match (entry.volume, entry.number) {
330            (Some(volume), Some(number)) => {
331                write!(out, ", {volume}({number})")?;
332                true
333            }
334            (Some(volume), None) => {
335                write!(out, ", {volume}")?;
336                true
337            }
338            (None, Some(number)) => {
339                write!(out, ", ({number})")?;
340                true
341            }
342            (None, None) => false,
343        };
344
345        if let Some(BibPages { first, last }) = entry.pages {
346            if has_volume_or_number {
347                write!(out, ":")?;
348            } else {
349                if last.is_some() {
350                    write!(out, ", pages ")?;
351                } else {
352                    write!(out, ", page ")?;
353                }
354            }
355            write!(out, "{first}")?;
356            if let Some(last) = last {
357                write!(out, "–{last}")?;
358            }
359        }
360
361        match (has_volume_or_number || entry.pages.is_some(), entry.year) {
362            (true, Some(year)) => {
363                write!(out, ", {year}.")?;
364            }
365            (true, None) => {
366                write!(out, ".")?;
367            }
368            (false, Some(year)) => {
369                if entry.journal.is_some() || entry.booktitle.is_some() || entry.series.is_some() {
370                    write!(out, ", {year}.")?;
371                } else {
372                    write!(out, " {year}.")?;
373                }
374            }
375            (false, None) => (),
376        };
377
378        writedoc! {out, r#"</li>"#}?;
379        Ok(())
380    })
381}
382
383fn write_index(out: &mut impl Write, doc: &Document, analysis: &Analysis) -> Result {
384    let title: Option<&Paragraph> = doc.parts.iter().find_map(|part| {
385        if let DocumentPart::Title(title) = part {
386            Some(title)
387        } else {
388            None
389        }
390    });
391
392    let head = display_head(display_title(title));
393    writedoc! {out, r#"
394        <!DOCTYPE html>
395        <html lang="en">
396        {head}
397        <body>
398    "#}?;
399
400    let config = &doc.config;
401
402    for part in doc.parts.iter() {
403        use DocumentPart::*;
404        match part {
405            FreeParagraph(p) => {
406                write!(out, "{}", display_paragraph(analysis, p))?;
407            }
408            Title(_) => (),
409            Author(_) => (),
410            Date() => (),
411            Maketitle() => {
412                if title.is_some() {
413                    let title = display_title(title);
414                    writedoc! {out, r#"
415                        <h1>{title}</h1>
416                    "#}?;
417                }
418            }
419            Section { name, label } => {
420                let label = display_label_id_attr(*label);
421                write!(out, "<h2{label}>\n")?;
422                let number = analysis
423                    .doc_part_numbering
424                    .get(&std::ptr::addr_of!(*part))
425                    .map(|s| s.as_str());
426                if let Some(number) = number {
427                    write!(out, "{number} ")?;
428                }
429                for part in name {
430                    write!(out, "{}", display_paragraph_part(analysis, part))?;
431                }
432                write!(out, "</h2>\n")?;
433            }
434            Subsection { name, label } => {
435                let label = display_label_id_attr(*label);
436                write!(out, "<h3{label}>\n")?;
437                let number = analysis
438                    .doc_part_numbering
439                    .get(&std::ptr::addr_of!(*part))
440                    .map(|s| s.as_str());
441                if let Some(number) = number {
442                    write!(out, "{number} ")?;
443                }
444                for part in name {
445                    write!(out, "{}", display_paragraph_part(analysis, part))?;
446                }
447                write!(out, "</h3>\n")?;
448            }
449            Abstract(ps) => {
450                write!(out, "<h2>Abstract</h2>\n")?;
451                for p in ps {
452                    write!(out, "{}", display_paragraph(analysis, p))?;
453                }
454            }
455            TheoremLike {
456                tag,
457                note,
458                content,
459                label,
460            } => {
461                let theorem_like_config = config
462                    .theorem_like_configs
463                    .iter()
464                    .find(|config| &config.tag == tag)
465                    .unwrap();
466                let theorem_style_class = match theorem_like_config.style {
467                    TheoremStyle::Theorem => "theorem-style-theorem",
468                    TheoremStyle::Definition => "theorem-style-definition",
469                    TheoremStyle::Remark => "theorem-style-remark",
470                };
471                let label = display_label_id_attr(*label);
472                let number = analysis
473                    .doc_part_numbering
474                    .get(&std::ptr::addr_of!(*part))
475                    .map(|s| s.as_str());
476                let header = display_theorem_header(
477                    analysis,
478                    &theorem_like_config.name,
479                    note.as_ref(),
480                    number,
481                );
482                writedoc! {out, r#"
483                    <div{label} class="theorem-like {theorem_style_class}">
484                    <div class="paragraph">
485                    {header}
486                "#}?;
487
488                let mut content = content.iter();
489                if let Some(parag) = content.next() {
490                    for part in parag {
491                        write!(out, "{}", display_paragraph_part(analysis, part))?;
492                    }
493                }
494                writedoc! {out, r#"
495                    </div>
496                "#}?;
497                for parag in content {
498                    write!(out, "{}", display_paragraph(analysis, parag))?;
499                }
500                writedoc! {out, r#"
501                    </div>
502                "#}?;
503            }
504            Proof(ps) => {
505                writedoc! {out, r#"
506                    <div class="proof">
507                    <div class="paragraph">
508                    <i class="proof">Proof.</i>
509                "#}?;
510                let mut ps = ps.iter();
511                if let Some(parag) = ps.next() {
512                    for part in parag {
513                        write!(out, "{}", display_paragraph_part(analysis, part))?;
514                    }
515                }
516                writedoc! {out, r#"
517                    </div>
518                "#}?;
519                for p in ps {
520                    write!(out, "{}", display_paragraph(analysis, p))?;
521                }
522                writedoc! {out, r#"
523                    </div>
524                "#}?;
525            }
526            Bibliography => {
527                writedoc! {out, r#"
528                    <h2>Bibliography</h2>
529                    <ol class="bibliography">
530                "#}?;
531                for entry in analysis.bib_entries.iter().copied() {
532                    let entry = display_bib_entry(entry);
533                    writedoc! {out, r#"
534                        {entry}
535                    "#}?;
536                }
537                writedoc! {out, r#"
538                    </ol>
539                "#}?;
540            }
541        }
542    }
543    writedoc! {out, r#"
544        </body>
545        </html>
546    "#}?;
547
548    Ok(())
549}
550
551const STYLE: &'static str = indoc! {r#"
552    html {
553        padding: 0.5em;
554    }
555    body {
556        font-family: "Computer Modern Serif", serif;
557        max-width: 600px;
558        margin: auto;
559    }
560
561    h4 {
562        display: inline;
563    }
564
565    .theorem-like {
566        margin-top: 0.5em;
567        margin-bottom: 0.5em;
568    }
569
570    .theorem-style-theorem {
571        font-style: italic;
572    }
573    .theorem-style-theorem h4 {
574        font-style: normal;
575    }
576
577    .theorem-style-remark h4 {
578        font-style: italic;
579        font-weight: normal;
580    }
581
582    .proof {
583        margin-top: 0.5em;
584        margin-bottom: 0.5em;
585    }
586
587    .inline-math {
588        vertical-align: baseline;
589        position: relative;
590    }
591
592    .display-math-row {
593        display: flex;
594        flex-direction: row;
595        margin-top: 0.5em;
596        margin-bottom: 0.5em;
597        overflow: auto;
598    }
599
600    .display-math-row > img {
601        margin: auto;
602    }
603
604    .display-math-row > span {
605        margin: auto 0;
606        display: inline-flex;
607        flex-direction: row-reverse;
608        padding-left: 1em;
609    }
610
611    .display-math-row > span:first-child {
612        visibility: hidden;
613    }
614
615    .bibliography {
616      counter-reset: list;
617    }
618
619    .bibliography > li {
620      counter-increment: list;
621    }
622
623    .bibliography > li::marker {
624      content: "["counter(list)"] ";
625    }"#};
626
627pub fn emit(root: &Path, doc: &Document, analysis: &Analysis) {
628    fs::create_dir_all(root).unwrap();
629
630    let mut index_src = String::new();
631    write_index(&mut index_src, &doc, &analysis).unwrap();
632
633    let index_path = root.join("index.html");
634    let mut index_file = std::fs::OpenOptions::new()
635        .write(true)
636        .truncate(true)
637        .create(true)
638        .open(index_path)
639        .unwrap();
640    write!(index_file, "{}", index_src).unwrap();
641
642    let style_path = root.join("style.css");
643    let mut style_path = std::fs::OpenOptions::new()
644        .write(true)
645        .truncate(true)
646        .create(true)
647        .open(style_path)
648        .unwrap();
649    write!(style_path, "{STYLE}").unwrap();
650}