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 }
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 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 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}