rustla/doctree/
larst_writer.rs

1/*!
2A submodule that contains the larst writer method of the doctree,
3and the patterns related to it. The prefix and postfix strings
4of each node are defined here.
5
6Copyright © 2020 Santtu Söderholm
7*/
8
9use std::io::Write;
10
11use super::*;
12use crate::common::AplusExerciseStatus;
13use crate::rustla_options::ruSTLaOptions;
14use crate::rustla_options::OutputStream;
15
16const LATEX_OPTION_DELIM: &str = ",";
17
18impl DocTree {
19
20    /// A function that writes a rusTLa doctree into `stdout` or a file, depending on the given output option.
21    pub fn write_to_larst(self, rustla_options: &ruSTLaOptions) {
22
23        // Generate output stream based on given options...
24        let mut output_stream: Box<dyn Write> = match rustla_options.shared_out_stream() {
25
26            OutputStream::StdOut => {
27                // Windows users beware: only valid UTF-8 accepted.
28                let stdout = std::io::stdout();
29                Box::new(stdout)
30            }
31
32            OutputStream::File => {
33
34                // Cannot write to file without knowing the file location
35                if self.file_folder.is_empty() || self.filename_stem.is_empty() {
36                    panic!("Cannot write to file without knowing the location. Computer says no...")
37                }
38
39                /// LaTeX file suffix
40                const TEX_FILE_SUFFIX: &str = ".tex";
41
42                let folder = &self.file_folder;
43                let mut object_file_path = std::path::PathBuf::from(folder);
44                object_file_path.push(self.filename_stem + TEX_FILE_SUFFIX);
45
46                eprintln!("Writing output to {:#?}...", object_file_path);
47
48                // TODO: Add check for file existence...
49                let object_file: std::fs::File = match std::fs::OpenOptions::new()
50                    .write(true)
51                    .truncate(true)
52                    .create(true)
53                    .open(object_file_path)
54                {
55                    Ok(file) => file,
56                    Err(e) => panic!("Could not open LarST file for writing purposes: {}", e),
57                };
58
59                Box::new(object_file)
60            }
61        };
62
63        self.tree.write_to_larst(&mut output_stream, rustla_options);
64
65        // If a file was requested and the write to LarST didnt panic!, create A+ class file...
66        // TODO: check for file existence.
67        match rustla_options.shared_out_stream() {
68
69            OutputStream::File if rustla_options.create_class_file() => {
70
71                /// The name of the A+ LaTeX class file
72                const APLUS_CLASS_FILE_NAME: &str = "aplus.cls";
73
74                let folder = &self.file_folder;
75                let mut aplus_class_file_path = std::path::PathBuf::from(folder);
76                aplus_class_file_path.push(APLUS_CLASS_FILE_NAME);
77                let mut aplus_class_file: std::fs::File = match std::fs::OpenOptions::new()
78                    .write(true)
79                    .truncate(true)
80                    .create(true)
81                    .open(aplus_class_file_path)
82                {
83                    Ok(file) => file,
84                    Err(e) => panic!("Could not open A+ class file for writing purposes: {}", e),
85                };
86
87                match aplus_class_file.write(aplus_cls_contents().as_bytes()){
88                    Ok(_) => {},
89                    Err(_) => panic!("Could not write to A+ class file after generating object code. Computer says no...")
90                };
91            }
92            _ => {}
93        }
94    }
95}
96
97impl TreeZipper {
98
99    /// This is the actual recursive function that goes over the tree zipper and writes each node
100    /// into its LarST string representation based on its `TreeNodeType`.
101    /// Starts out by calling `TreeNodeType`-specific pre-order action,
102    /// then recursively calls itself for the children of the node and
103    /// finishes by calling a post-order action on `self`.
104    fn write_to_larst(mut self, output_stream: &mut Box<dyn Write>, rustla_options: &ruSTLaOptions) {
105
106        self = self.walk_to_root(); // Start out by walking to root.
107
108        self.shared_node().larst_pre_order_write(output_stream, rustla_options);
109
110        if let Some(children) = self.shared_node().shared_children() {
111            for child in children {
112                child.write_to_larst(output_stream, rustla_options);
113            }
114        }
115
116        self.shared_node().larst_post_order_write(output_stream, rustla_options);
117    }
118}
119
120impl TreeNode {
121
122    /// Recursively writes a node and its children (and the children of those, etc.) to LarST.
123    fn write_to_larst(&self, output_stream: &mut Box<dyn Write>, rustla_options: &ruSTLaOptions) {
124
125        self.larst_pre_order_write(output_stream, rustla_options);
126
127        if let Some(children) = self.shared_children() {
128            for child in children {
129                child.write_to_larst(output_stream, rustla_options);
130            }
131        }
132
133        self.larst_post_order_write(output_stream, rustla_options);
134    }
135
136    /// Calls the pre-order LarST writer method of the contained `TreeNodeType` variant.
137    /// output is directed to the given file.
138    fn larst_pre_order_write(&self, output_stream: &mut Box<dyn Write>, rustla_options: &ruSTLaOptions) {
139
140        let refnames = self.shared_target_labels().as_ref();
141
142        let pre_string = self.shared_data().larst_pre_order_string(refnames, rustla_options);
143        match output_stream.write(pre_string.as_bytes()) {
144            Ok(_) => {}
145            Err(_) => panic!(
146                "Could not write the prefix string \"{}\" to file. Computer says no...",
147                pre_string
148            ),
149        };
150    }
151
152    /// Calls the post-order LarST writer method of the contained `TreeNodeType` variant.
153    /// output is directed to the given file.
154    fn larst_post_order_write(&self, output_stream: &mut Box<dyn Write>, rustla_options: &ruSTLaOptions) {
155
156        let refnames = self.shared_target_labels().as_ref();
157
158        let post_string = self.shared_data().larst_post_order_string(refnames, rustla_options);
159        match output_stream.write(post_string.as_bytes()) {
160            Ok(_) => {}
161            Err(_) => panic!(
162                "Could not write the prefix string \"{}\" to file. Computer says no...",
163                post_string
164            ),
165        };
166    }
167
168
169    /// Generates a single string of LarST labels from contained reference names.
170    fn ref_names_into_larst_labels(&self) -> String {
171        if let Some(refnames) = self.shared_target_label() {
172            let mut targets = String::new();
173            for refname in refnames.iter() {
174                targets += &format!("\\label{{{}}}\n", refname);
175            }
176            targets
177        } else {
178            String::new()
179        }
180    }
181}
182
183impl TreeNodeType {
184
185    /// Defines the text pattern each `TreeNodeType` variant starts with.
186    fn larst_pre_order_string(&self, ref_names: Option<&Vec<String>>, rustla_options: &ruSTLaOptions) -> String {
187        let pre_string = match self {
188            Self::Abbreviation { .. } => todo!(),
189            Self::AbsoluteURI { text } => {
190                format!(r"\url{{{}}}", text)
191            }
192            Self::Acronym { .. } => todo!(),
193            Self::Address => todo!(),
194            Self::Admonition {
195                content_indent,
196                classes,
197                name,
198                variant,
199            } => {
200                use crate::doctree::directives::AdmonitionType;
201
202                match variant {
203                    AdmonitionType::Admonition { title } => {
204                        format!("\\begin{{admonition}}{{{}}}\n", title)
205                    }
206                    _ => format!("\\begin{{{}}}\n", variant.to_string()),
207                }
208            }
209            Self::Attribution { raw_text } => {
210                format!("-- {}", raw_text)
211            }
212            Self::Author { .. } => todo!(),
213            Self::Authors { .. } => todo!(),
214            Self::AutomaticSectionNumbering { .. } => todo!(),
215            Self::BlockQuote { body_indent } => {
216                format!("\\begin{{quotation}}\n")
217            }
218            Self::BulletList {
219                bullet,
220                bullet_indent,
221                text_indent,
222            } => {
223                format!("\\begin{{itemize}}\n")
224            }
225            Self::BulletListItem {
226                bullet,
227                bullet_indent,
228                text_indent,
229            } => {
230                format!("\\item ")
231            }
232            Self::Caption { indent } => {
233                format!(r"\captionof{{figure}}{{")
234            }
235            Self::Citation { body_indent, label } => {
236                format!("\\bibitem{{{}}}\n", label)
237            }
238            Self::CitationReference {
239                displayed_text,
240                target_label,
241            } => {
242                format!("\\cite{{{}}}", displayed_text)
243            }
244            Self::Class { .. } => "".to_string(),
245            Self::Classifier { .. } => todo!(),
246            Self::Code {
247                text,
248                language,
249                name,
250                class,
251                number_lines,
252            } => {
253                let lang = if let Some(lang) = language {
254                    format!("[{}]", lang)
255                } else {
256                    "".to_string()
257                };
258                format!("\\begin{{codeblock}}{}\n", lang)
259            }
260            Self::ColSpec { .. } => todo!(),
261            Self::Comment { text } => {
262                if let Some(comment) = text {
263                    comment
264                        .lines()
265                        .fold(String::new(), |a, b| a + "% " + b + "\n")
266                        + "\n"
267                } else {
268                    String::new()
269                }
270            }
271            Self::CompoundParagraph { .. } => todo!(),
272            Self::Contact { .. } => todo!(),
273            Self::Container { .. } => todo!(),
274            Self::Copyright { .. } => todo!(),
275            Self::CSVTable { .. } => todo!(),
276            Self::Date => todo!(),
277            Self::Decoration => todo!(),
278            Self::Definition => todo!(),
279            Self::DefinitionList { term_indent } => {
280                format!("\\begin{{itemize}}\n")
281            }
282            Self::DefinitionListItem {
283                term,
284                classifiers,
285                body_indent,
286            } => {
287                let classifiers = if classifiers.is_empty() {
288                    "".to_string()
289                } else {
290                    format!(": {}", classifiers.join(", "))
291                };
292
293                format!("\\item \\textbf{{{}}}{}\n\n", term, classifiers)
294            }
295            Self::Description => todo!(),
296            Self::DocInfo => todo!(),
297            Self::DoctestBlock { .. } => todo!(),
298            Self::Document { .. } => if rustla_options.is_full_document() {
299                format!("\\documentclass{{aplus}}\n\\begin{{document}}\n\n")
300            } else {
301                String::new()
302            },
303            Self::Emphasis { text } => {
304                format!("\\textit{{{}}}", text)
305            }
306            Self::EmptyLine => {
307                format!("")
308            }
309            Self::Entry { .. } => {
310                format!("")
311            }
312            Self::EnumeratedList {
313                delims,
314                kind,
315                start_index,
316                n_of_items,
317                enumerator_indent,
318            } => {
319                format!("\\begin{{enumerate}}\n")
320            }
321            Self::EnumeratedListItem {
322                delims,
323                kind,
324                index_in_list,
325                enumerator_indent,
326                text_indent,
327            } => {
328                format!("\\item ")
329            }
330            Self::ExternalHyperlinkTarget { .. } => todo!(),
331            Self::Field => unimplemented!(),
332            Self::FieldBody { .. } => unimplemented!(),
333            Self::FieldList { marker_indent } => {
334                format!("\\begin{{itemize}}\n")
335            }
336            Self::FieldListItem {
337                raw_marker_name,
338                marker_name_as_inline_nodes,
339                ..
340            } => {
341                format!("\\item \\textbf{{{}}}\n\n", raw_marker_name)
342            }
343            Self::Figure {
344                body_indent,
345                align,
346                figwidth,
347                figclass,
348                ..
349            } => {
350                let mut options = Vec::<String>::new();
351                if let Some(alignment) = align {
352                    options.push(alignment.to_string())
353                }
354                if let Some(width) = figwidth {
355                    options.push(format!("figwidth={}", width.to_string()))
356                }
357                if let Some(class) = figclass {
358                    options.push(format!("figclass={}", class.to_string()))
359                }
360
361                let options_string = if !options.is_empty() {
362                    format!("[{}]", options.join(","))
363                } else {
364                    String::new()
365                };
366
367                format!("\\begin{{center}}\n")
368            }
369            Self::Footer { .. } => todo!(),
370            Self::Footnote { kind, label, target, .. } => {
371                format!("\\footnote{{\n")
372            },
373            Self::FootnoteReference { .. } => {
374                format!("\\footnotemark")
375            },
376            Self::Header { .. } => todo!(),
377            Self::Generated => todo!(),
378            Self::Image {
379                uri,
380                alt,
381                height,
382                width,
383                scale,
384                align,
385                target,
386                name,
387                class,
388                ..
389            } => {
390                let mut options = Vec::<String>::new();
391
392                if let Some(val) = alt {
393                    options.push(format!("{}", val))
394                };
395                if let Some(h) = height {
396                    options.push(format!("height={}", h.to_string()))
397                }
398                if let Some(w) = width {
399                    options.push(format!("width={}", w.to_string()))
400                }
401                if let Some(val) = scale {
402                    options.push(format!("scale={0:.2}%", val))
403                }
404                if let Some(val) = align {
405                    options.push(format!("align={}", val.to_string()))
406                }
407
408                let options = if options.is_empty() {
409                    String::new()
410                } else {
411                    format!("[{}]", options.join(LATEX_OPTION_DELIM))
412                };
413
414                format!("\\includegraphics{}{{{}}}", options, uri)
415            }
416            Self::Include { uri, .. } => {
417                // Options ignored for now...
418                format!("\\input{{{}}}\n", uri)
419            }
420            Self::IndirectHyperlinkTarget { .. } => todo!(),
421            Self::Inline { .. } => todo!(),
422            Self::InlineTarget { .. } => todo!(),
423            Self::InterpretedText { role, content } => {
424                format!("\\{}{{{}}}", role, content)
425            },
426            Self::Label { .. } => todo!(),
427            Self::Legend { .. } => todo!(),
428            Self::Line { .. } => todo!(),
429            Self::LineBlock { .. } => todo!(),
430            Self::ListTable {
431                title,
432                widths,
433                width,
434                header_rows,
435                stub_columns,
436                align,
437                ..
438            } => {
439                let widths = if let Some(widths) = widths {
440                    match widths {
441                        TableColWidths::Auto => String::new(),
442                        TableColWidths::Columns(vals) => {
443                            let mut col_widths = Vec::<String>::with_capacity(vals.len());
444                            for val in vals {
445                                col_widths.push(format!("p{{{0:.2}\\textwidth}}", *val));
446                            }
447                            col_widths.join("")
448                        }
449                    }
450                } else {
451                    panic!("Columns widths need to be set for all list tables. Computer says no...")
452                };
453
454                // Generating the option string
455                let mut options = Vec::<String>::new();
456                if let Some(title) = title { options.push(format!("title={}", title)) }
457                let options_string = if ! options.is_empty() {
458                    let options = options.join(LATEX_OPTION_DELIM);
459                    format!("[{}]", options)
460                } else {
461                    String::new()
462                };
463
464                format!("\\begin{{tabular}}{}{{{}}}\n", options_string, widths)
465            }
466            Self::Literal { text } => format!("\\texttt{{{}}}", text),
467            Self::LiteralBlock { text } => {
468                use crate::utf8_to_latex::unicode_text_to_latex;
469                format!("\\begin{{codeblock}}\n{}", unicode_text_to_latex(text))
470            }
471            Self::Math { text, class, name } => {
472                format!(r"\({}\)", crate::utf8_to_latex::unicode_math_to_latex(text))
473            }
474            Self::MathBlock {
475                math_block,
476                name,
477                class,
478            } => {
479                let ref_labels = self.anchor_string(ref_names);
480                format!(
481                    "{}\\begin{{equation}}\n{}\n",
482                    ref_labels, crate::utf8_to_latex::unicode_math_to_latex(math_block)
483                )
484            }
485            Self::OptionList { .. } => todo!(),
486            Self::OptionListItem { .. } => todo!(),
487            Self::OptionString { .. } => todo!(),
488            Self::Organization { .. } => todo!(),
489            Self::Paragraph { .. } => "".to_string(),
490            Self::ParsedLiteralBlock { .. } => todo!(),
491            Self::Pending { .. } => todo!(),
492            Self::Problematic { .. } => todo!(),
493            Self::Raw { .. } => "\\begin{codeblock}\n".to_string(),
494            Self::Reference {
495                displayed_text,
496                reference,
497            } => {
498
499                use crate::common::Reference;
500
501                match reference {
502                    Reference::Internal(ref_str) => {
503                        if let Some(text) = displayed_text {
504                            format!("\\hyperref[{}]{{{}}}", ref_str, text)
505                        } else {
506                            format!("\\ref{{{}}}", ref_str)
507                        }
508                    }
509                    Reference::URI(ref_str) => {
510                        if let Some(text) = displayed_text {
511                            format!("\\href{{{}}}{{{}}}", ref_str, text)
512                        } else {
513                            format!("\\url{{{}}}", ref_str)
514                        }
515                    }
516                    Reference::EMail(ref_str) => {
517                        if let Some(text) = displayed_text {
518                            format!("\\href{{{}}}{{{}}}", ref_str, text)
519                        } else {
520                            format!("\\href{{{}}}{{{}}}", ref_str, ref_str)
521                        }
522                    }
523                }
524            }
525            Self::Revision { .. } => todo!(),
526            Self::Row { .. } => todo!(),
527            Self::Rubric { .. } => todo!(),
528            Self::Section {
529                title_text,
530                level,
531                line_style,
532            } => {
533                let (command, subs) = if *level == 1 {
534                    ("chapter", "")
535                } else if *level == 2 {
536                    ("section", "")
537                } else if *level == 3 {
538                    ("section", "sub")
539                } else {
540                    ("section", "subsub")
541                };
542
543                let anchors = self.anchor_string(ref_names);
544
545                format!("{}\\{}{}{{{}}}\n\n", anchors, subs, command, title_text)
546            }
547            Self::Sidebar { .. } => todo!(),
548            Self::Status { .. } => todo!(),
549            Self::StrongEmphasis { text } => {
550                format!("\\textbf{{{}}}", text)
551            }
552            Self::Subscript { text } => {
553                format!(r"\textsubscript{{{}}}", text)
554            }
555            Self::SubstitutionDefinition { .. } => todo!(),
556            Self::SubstitutionReference {
557                substitution_label,
558                target_label,
559            } => {
560                todo!()
561            }
562            Self::Subtitle { .. } => todo!(),
563            Self::Superscript { text } => {
564                format!(r"\textsuperscript{{{}}}", text)
565            }
566            Self::SystemMessage { .. } => todo!(),
567            Self::Table { .. } => todo!(),
568            Self::Target { .. } => todo!(),
569            Self::TBody { .. } => "".to_string(),
570            Self::Term { .. } => todo!(),
571            Self::Text { text } => {
572                format!("{}", text)
573            }
574            Self::TGroup { .. } => todo!(),
575            Self::THead { .. } => todo!(),
576            Self::TRow { .. } => "".to_string(),
577            Self::Title { .. } => todo!(),
578            Self::TitleReference {
579                displayed_text,
580                target_label,
581            } => {
582                format!("\\hyperref[{}]{{{}}}", target_label, displayed_text)
583            }
584            Self::Topic { .. } => todo!(),
585            Self::Transition {} => {
586                format!("\\hrulefill\n")
587            }
588            Self::UnknownDirective {
589                directive_name,
590                argument,
591                options,
592                ..
593            } => {
594                let arg_str: String = if argument.trim().is_empty() {
595                    String::new()
596                } else {
597                    format!("{{{}}}", argument)
598                };
599                let mut option_vec = Vec::new();
600                for (key, val) in options.keys().zip(options.values()) {
601                    option_vec.push(format!("{}={}", key, val))
602                }
603                let option_str = if option_vec.is_empty() {
604                    String::new()
605                } else {
606                    format!("[{}]", option_vec.join(LATEX_OPTION_DELIM))
607                };
608
609                format!(
610                    "\\begin{{{}}}{}{}\n",
611                    directive_name.to_lowercase(),
612                    option_str,
613                    arg_str
614                )
615            }
616            Self::Version { .. } => todo!(),
617            Self::WhiteSpace { text } => {
618                format!("{}", text)
619            }
620
621            // ============================
622            //  Sphinx specific directives
623            // ============================
624            Self::SphinxOnly {
625                expression,
626                body_indent,
627            } => {
628                format!("\\begin{{only}}[{}]\n", expression)
629            }
630
631            Self::SphinxCodeBlock {
632                language,
633                linenos,
634                lineno_start,
635                emphasize_lines,
636                caption,
637                name,
638                dedent,
639                force,
640                code_text,
641            } => {
642                let mut options = Vec::<String>::new();
643
644                if *linenos {
645                    options.push(String::from("linenos"))
646                }
647                if let Some(start_line) = lineno_start {
648                    options.push(format!("lineno-start={}", start_line.to_string()))
649                }
650                if let Some(line_numbers) = emphasize_lines {
651                    let line_number_strings = line_numbers
652                        .iter()
653                        .map(|line| line.to_string())
654                        .collect::<Vec<String>>()
655                        .join(",");
656                    options.push(format!("emphasize_lines={}", line_number_strings))
657                }
658                if let Some(caption) = caption {
659                    let latex_caption = crate::utf8_to_latex::unicode_text_to_latex(caption);
660                    options.push(format!("caption={}", latex_caption))
661                }
662                if let Some(name) = name {
663                    let normalized_refname = crate::common::normalize_refname(name);
664                    options.push(format!("name"))
665                }
666                if let Some(dedent) = dedent {
667                    // Remove indentation from code_text here?
668                }
669
670                if *force {
671                    options.push(format!("force"))
672                }
673
674                let option_string = options.join(",");
675
676                // LarST does not support many of the given options yet, so they are not written to the resulting file...
677                format!("\\begin{{codeblock}}[{}]\n{}", language, code_text)
678            }
679
680            // ========================
681            //  A+ specific directives
682            // ========================
683            Self::AplusPOI {
684                id,
685                title,
686                previous,
687                next,
688                hidden,
689                class,
690                height,
691                columns,
692                bgimg,
693                not_in_slides,
694                not_in_book,
695                no_poi_box,
696                ..
697            } => {
698                let mut options = String::new();
699
700                if let Some(option) = id {
701                    options = options + "id=" + option + LATEX_OPTION_DELIM
702                };
703                if let Some(option) = previous {
704                    options = options + "previous=" + option + LATEX_OPTION_DELIM
705                };
706                if let Some(option) = next {
707                    options = options + "next=" + option + LATEX_OPTION_DELIM
708                };
709                if let Some(option) = hidden {
710                    options = options + "hidden" + LATEX_OPTION_DELIM
711                };
712                if let Some(option) = class {
713                    options = options + "class=" + option + LATEX_OPTION_DELIM
714                };
715                if let Some(option) = height {
716                    let height = rst_length_to_string(option);
717                    options = options + "height=" + &height + LATEX_OPTION_DELIM
718                };
719                if let Some(option) = columns {
720                    options = options + "columns=" + option + LATEX_OPTION_DELIM
721                };
722                if let Some(option) = bgimg {
723                    options = options + "bgimg=" + option + LATEX_OPTION_DELIM
724                };
725                if let Some(option) = not_in_slides {
726                    options = options + "not_in_slides" + LATEX_OPTION_DELIM
727                };
728                if let Some(option) = not_in_book {
729                    options = options + "not_in_book" + LATEX_OPTION_DELIM
730                };
731                if let Some(option) = no_poi_box {
732                    options = options + "no_poi_box" + LATEX_OPTION_DELIM
733                };
734
735                if !options.is_empty() {
736                    options = format!("[{}]", options.as_str())
737                };
738                format!("\\begin{{poi}}{}{{{}}}\n\n", options, title)
739            }
740            Self::AplusColBreak => "\\newcol\n\n".to_string(),
741            Self::AplusQuestionnaire {
742                max_points,
743                key,
744                points_from_children,
745                difficulty,
746                submissions,
747                points_to_pass,
748                feedback,
749                title,
750                no_override,
751                pick_randomly,
752                preserve_questions_between_attempts,
753                category,
754                status,
755                reveal_model_at_max_submissions,
756                show_model,
757                allow_assistant_viewing,
758                allow_assistant_grading,
759                ..
760            } => {
761                let max_points = if let Some(points) = max_points {
762                    points
763                } else {
764                    points_from_children
765                };
766
767                format!("\\begin{{quiz}}{{{}}}{{{}}}\n", key, *max_points)
768            }
769            Self::AplusPickOne {
770                points,
771                class,
772                required,
773                key,
774                dropdown,
775                ..
776            } => {
777                let mut options = String::new();
778                if let Some(option) = class {
779                    options = options + "id=" + option + LATEX_OPTION_DELIM
780                };
781                if *required {
782                    options = options + "required" + LATEX_OPTION_DELIM
783                };
784                if let Some(option) = key {
785                    options = options + "key=" + option + LATEX_OPTION_DELIM
786                };
787                // if *dropdown { options = options + "dropdown" + OPTION_DELIM };
788
789                let options = if !options.is_empty() {
790                    format!("[{}]", options)
791                } else {
792                    options
793                };
794
795                format!("\\begin{{pick}}{}{{one}}{{{}}}\n", options, points)
796            }
797            Self::AplusPickAny {
798                points,
799                class,
800                required,
801                key,
802                partial_points,
803                randomized,
804                correct_count,
805                preserve_questions_between_attempts,
806                ..
807            } => {
808                let mut options = String::new();
809                if let Some(option) = class {
810                    options = options + "id=" + option + LATEX_OPTION_DELIM
811                };
812                if *required {
813                    options = options + "required" + LATEX_OPTION_DELIM
814                };
815                if let Some(option) = key {
816                    options = options + "key=" + option + LATEX_OPTION_DELIM
817                };
818                if *partial_points {
819                    options = options + "partial-points" + LATEX_OPTION_DELIM
820                };
821                if *randomized {
822                    options = options + "randomized" + LATEX_OPTION_DELIM
823                };
824                if let Some(correct_count) = correct_count {
825                    options = options
826                        + "correct-count="
827                        + correct_count.to_string().as_str()
828                        + LATEX_OPTION_DELIM
829                };
830                if *preserve_questions_between_attempts {
831                    options = options + "preserve-questions-between-attempts" + LATEX_OPTION_DELIM
832                };
833
834                if !options.is_empty() {
835                    options = format!("[{}]", options)
836                }
837
838                format!("\\begin{{pick}}{}{{any}}{{{}}}\n", options, points)
839            }
840            Self::AplusFreeText {
841                points,
842                compare_method,
843                model_answer,
844                required,
845                class,
846                key,
847                length,
848                height,
849                ..
850            } => {
851                format!(
852                    "\\begin{{freetext}}{{{}}}{{{}}}{{{}}}\n",
853                    compare_method, points, model_answer
854                )
855            }
856            Self::AplusPickChoices { .. } => "\\begin{answers}\n".to_string(),
857            Self::AplusPickChoice {
858                label,
859                is_correct,
860                is_pre_selected,
861                is_neutral,
862            } => {
863                let is_correct = if *is_neutral {
864                    "\\undet"
865                } else if *is_correct {
866                    "\\right"
867                } else {
868                    "\\wrong"
869                };
870                format!("{} ", is_correct)
871            }
872            Self::AplusQuestionnaireHints { .. } => "".to_string(),
873            Self::AplusQuestionnaireHint {
874                label,
875                show_when_not_selected,
876                question_type,
877            } => {
878                let show_when_not_selected = if *show_when_not_selected { "" } else { "!" };
879
880                use crate::common::AplusQuestionnaireType;
881
882                let reference = match question_type {
883                    AplusQuestionnaireType::PickOne | AplusQuestionnaireType::PickAny => {
884                        format!("\\ref{{{}}}", label)
885                    }
886                    AplusQuestionnaireType::FreeText => String::new(),
887                };
888
889                format!("\\feedback{{{}{}}}{{", show_when_not_selected, reference)
890            }
891            Self::AplusSubmit {
892                body_indent,
893                key,
894                difficulty,
895                max_points,
896                config,
897                submissions,
898                points_to_pass,
899                class,
900                title,
901                category,
902                status,
903                ajax,
904                allow_assistant_viewing,
905                allow_assistant_grading,
906                quiz,
907                url,
908                radar_tokenizer,
909                radar_minimum_match_tokens,
910                lti,
911                lti_resource_link_id,
912                lti_open_in_iframe,
913                lti_aplus_get_and_post,
914            } => {
915                // Read relevant options
916
917                let mut options = Vec::<String>::new();
918                if !config.is_empty() { options.push(format!("config={}", config)) }
919                options.push(format!("submissions={}", *submissions));
920                options.push(format!("points-to-pass={}", *points_to_pass));
921                if !class.is_empty() { options.push(format!("class={}", class)) }
922                if !title.is_empty() { options.push(format!("title={}", title)) };
923                if !category.is_empty() { options.push(format!("category={}", category)) }
924                match status {
925                    AplusExerciseStatus::Ready => {
926                        options.push(String::from("status=ready"))
927                    }
928                    AplusExerciseStatus::Unlisted => {
929                        options.push(String::from("status=unlisted"))
930                    }
931                    AplusExerciseStatus::Hidden => {
932                        options.push(String::from("status=hidden"))
933                    }
934                    AplusExerciseStatus::Enrollment => {
935                        options.push(String::from("status=enrollment"))
936                    }
937                    AplusExerciseStatus::EnrollmentExt => {
938                        options.push(String::from("status=enrollment_ext"))
939                    }
940                    AplusExerciseStatus::Maintenance => {
941                        options.push(String::from("status=maintenance"))
942                    }
943                };
944                if *ajax { options.push(String::from("ajax")) };
945                if *allow_assistant_viewing { options.push(String::from("allow-assistant-viewing")) };
946                if *allow_assistant_grading { options.push(String::from("allow-assistant-grading")) };
947                if *quiz { options.push(String::from("quiz")) };
948                if !lti.is_empty() { options.push(String::from("lti")) };
949                if !lti_resource_link_id.is_empty() { options.push(format!("resource_link_id={}", lti_resource_link_id))};
950                if *lti_open_in_iframe { options.push(String::from("lti_open_in_iframe")) };
951                if *lti_aplus_get_and_post { options.push(String::from("lti_aplus_get_and_post")) };
952
953                let option_string = format!("[{}]", options.join(LATEX_OPTION_DELIM));
954
955                format!(
956                    "\\begin{{submit}}{}{{{}}}{{{}}}\n",
957                    option_string.trim(), key, max_points
958                )
959            }
960
961            Self::AplusActiveElementInput {
962                key_for_input,
963                title,
964                default,
965                class,
966                width,
967                height,
968                clear,
969                input_type,
970                file,
971            } => {
972                use crate::common::{AplusActiveElementClear, AplusActiveElementInputType};
973
974                let mut options = String::new();
975
976                let title = if let Some(title) = title { title } else { "" };
977                if let Some(option) = default {
978                    options = options + "default=" + option + LATEX_OPTION_DELIM
979                }
980                if let Some(option) = class {
981                    options = options + "class=" + option + LATEX_OPTION_DELIM
982                }
983                if let Some(option) = width {
984                    let width = rst_length_to_string(option);
985                    options = options + "width=" + &width + LATEX_OPTION_DELIM
986                }
987                if let Some(option) = height {
988                    let height = rst_length_to_string(option);
989                    options = options + "height=" + &height + LATEX_OPTION_DELIM
990                }
991                if let Some(option) = clear {
992                    match option {
993                        AplusActiveElementClear::Both => {
994                            options = options + "clear=both" + LATEX_OPTION_DELIM
995                        }
996                        AplusActiveElementClear::Left => {
997                            options = options + "clear=left" + LATEX_OPTION_DELIM
998                        }
999                        AplusActiveElementClear::Right => {
1000                            options = options + "clear=right" + LATEX_OPTION_DELIM
1001                        }
1002                    }
1003                }
1004                if let Some(option) = input_type {
1005                    match option {
1006                        AplusActiveElementInputType::File => {
1007                            options = options + "type=file" + LATEX_OPTION_DELIM
1008                        }
1009                        AplusActiveElementInputType::Clickable => {
1010                            options = options + "type=clickable" + LATEX_OPTION_DELIM
1011                        }
1012                        AplusActiveElementInputType::Dropdown(option_string) => {
1013                            options =
1014                                options + "type=dropdown:" + option_string + LATEX_OPTION_DELIM
1015                        }
1016                    }
1017                }
1018                if let (Some(input_type), Some(file)) = (input_type, file) {
1019                    if let AplusActiveElementInputType::Clickable = input_type {
1020                        options = options + "file=" + file + LATEX_OPTION_DELIM;
1021                    } else {
1022                        eprintln!("LarST writer found an alleged file path but active element input type not \"clickable\". Ignoring...")
1023                    }
1024                }
1025
1026                if !options.is_empty() {
1027                    options = format!("[{}]", options)
1028                }
1029
1030                format!("\\aeinput{}{{{}}}{{{}}}", options, key_for_input, title)
1031            }
1032
1033            Self::AplusActiveElementOutput {
1034                key_for_output,
1035                config,
1036                inputs,
1037                title,
1038                class,
1039                width,
1040                height,
1041                clear,
1042                output_type,
1043                submissions,
1044                scale_size,
1045                status,
1046            } => {
1047                let mut options = String::new();
1048                let title = if let Some(title) = title { title } else { "" };
1049
1050                use crate::common::{AplusActiveElementClear, AplusActiveElementOutputType};
1051
1052                options = options + "config=" + config + LATEX_OPTION_DELIM;
1053                // options = options + "inputs=" + inputs + LATEX_OPTION_DELIM;
1054                if let Some(option) = class {
1055                    options = options + "class=" + option + LATEX_OPTION_DELIM
1056                }
1057                if let Some(option) = width {
1058                    let width = rst_length_to_string(option);
1059                    options = options + "width=" + &width + LATEX_OPTION_DELIM
1060                }
1061                if let Some(option) = height {
1062                    let height = rst_length_to_string(option);
1063                    options = options + "height=" + &height + LATEX_OPTION_DELIM
1064                }
1065                if let Some(option) = clear {
1066                    match option {
1067                        AplusActiveElementClear::Both => {
1068                            options = options + "clear=both" + LATEX_OPTION_DELIM
1069                        }
1070                        AplusActiveElementClear::Left => {
1071                            options = options + "clear=left" + LATEX_OPTION_DELIM
1072                        }
1073                        AplusActiveElementClear::Right => {
1074                            options = options + "clear=right" + LATEX_OPTION_DELIM
1075                        }
1076                    }
1077                }
1078                match output_type {
1079                    AplusActiveElementOutputType::Text => {
1080                        options = options + "type=text" + LATEX_OPTION_DELIM
1081                    }
1082                    AplusActiveElementOutputType::Image => {
1083                        options = options + "type=image" + LATEX_OPTION_DELIM
1084                    }
1085                }
1086                if let Some(option) = submissions {
1087                    options = options + "submissions=" + &option.to_string() + LATEX_OPTION_DELIM
1088                }
1089                if *scale_size {
1090                    options = options + "scale-size" + LATEX_OPTION_DELIM
1091                }
1092                match status {
1093                    AplusExerciseStatus::Ready => {
1094                        options = options + "status=ready" + LATEX_OPTION_DELIM
1095                    }
1096                    AplusExerciseStatus::Unlisted => {
1097                        options = options + "status=unlisted" + LATEX_OPTION_DELIM
1098                    }
1099                    AplusExerciseStatus::Hidden => {
1100                        options = options + "status=hidden" + LATEX_OPTION_DELIM
1101                    }
1102                    AplusExerciseStatus::Enrollment => {
1103                        options = options + "status=enrollment" + LATEX_OPTION_DELIM
1104                    }
1105                    AplusExerciseStatus::EnrollmentExt => {
1106                        options = options + "status=enrollment-ext" + LATEX_OPTION_DELIM
1107                    }
1108                    AplusExerciseStatus::Maintenance => {
1109                        options = options + "status=maintenance" + LATEX_OPTION_DELIM
1110                    }
1111                }
1112
1113                if !options.is_empty() {
1114                    options = format!("[{}]", options)
1115                }
1116
1117                format!(
1118                    "\\aeoutput{}{{{}}}{{{}}}{{{}}}",
1119                    options, key_for_output, inputs, title
1120                )
1121            }
1122        };
1123
1124        pre_string
1125    }
1126
1127    /// Defines the text pattern each `TreeNodeType` variant ends with.
1128    fn larst_post_order_string(&self, ref_names:  Option<&Vec<String>>, rustla_options: &ruSTLaOptions) -> String {
1129        let post_string = match self {
1130            Self::Abbreviation { .. } => todo!(),
1131            Self::AbsoluteURI { .. } => "".to_string(),
1132            Self::Acronym { .. } => todo!(),
1133            Self::Address => todo!(),
1134            Self::Admonition { variant, .. } => {
1135                use crate::doctree::directives::AdmonitionType;
1136                match variant {
1137                    AdmonitionType::Admonition { title } => format!("\\end{{admonition}}\n\n"),
1138                    _ => format!("\\end{{{}}}\n\n", variant.to_string()),
1139                }
1140            }
1141            Self::Attribution { .. } => "\n".to_string(),
1142            Self::Author { .. } => todo!(),
1143            Self::Authors { .. } => todo!(),
1144            Self::AutomaticSectionNumbering { .. } => todo!(),
1145            Self::BlockQuote { .. } => "\\end{quotation}\n\n".to_string(),
1146            Self::BulletList { .. } => format!("\\end{{itemize}}\n\n"),
1147            Self::BulletListItem { .. } => "".to_string(),
1148            Self::Caption { .. } => "}\n".to_string(),
1149            Self::Citation { .. } => "\n".to_string(),
1150            Self::CitationReference { .. } => "".to_string(),
1151            Self::Class { .. } => "".to_string(),
1152            Self::Classifier { .. } => todo!(),
1153            Self::Code { .. } => "\\end{codeblock}\n\n".to_string(),
1154            Self::ColSpec { .. } => todo!(),
1155            Self::Comment { .. } => "".to_string(),
1156            Self::CompoundParagraph { .. } => "\n".to_string(),
1157            Self::Contact { .. } => todo!(),
1158            Self::Container { .. } => todo!(),
1159            Self::Copyright { .. } => todo!(),
1160            Self::CSVTable { .. } => todo!(),
1161            Self::Date => todo!(),
1162            Self::Decoration => todo!(),
1163            Self::Definition => todo!(),
1164            Self::DefinitionList { .. } => "\\end{itemize}\n\n".to_string(),
1165            Self::DefinitionListItem { .. } => "\n".to_string(),
1166            Self::Description => todo!(),
1167            Self::DocInfo => todo!(),
1168            Self::DoctestBlock { .. } => todo!(),
1169            Self::Document { .. } => if rustla_options.is_full_document() {
1170                "\\end{document}\n".to_string()
1171            } else {
1172                String::new()
1173            },
1174            Self::Emphasis { .. } => "".to_string(),
1175            Self::EmptyLine => "".to_string(),
1176            Self::Entry { is_last } => {
1177                let suffix = if *is_last { "" } else { "&\n" };
1178                format!("{}", suffix)
1179            }
1180            Self::EnumeratedList { .. } => "\\end{enumerate}\n\n".to_string(),
1181            Self::EnumeratedListItem { .. } => "".to_string(),
1182            Self::ExternalHyperlinkTarget { .. } => "\n".to_string(),
1183            Self::Field => todo!(),
1184            Self::FieldBody { .. } => todo!(),
1185            Self::FieldList { .. } => "\\end{itemize}\n\n".to_string(),
1186            Self::FieldListItem { .. } => "\n".to_string(),
1187            Self::Figure { .. } => {
1188                let anchors = self.anchor_string(ref_names);
1189                format!("{}\\end{{center}}\n\n", anchors)
1190            },
1191            Self::Footer { .. } => todo!(),
1192            Self::Footnote { .. } => String::from("}\n\n"),
1193            Self::FootnoteReference { .. } => String::new(),
1194            Self::Header { .. } => todo!(),
1195            Self::Generated => todo!(),
1196            Self::Image { inline, .. } => {
1197                if *inline {
1198                    String::new()
1199                } else {
1200                    String::from("\n")
1201                }
1202            },
1203            Self::Include { .. } => "".to_string(),
1204            Self::IndirectHyperlinkTarget { .. } => todo!(),
1205            Self::Inline { .. } => todo!(),
1206            Self::InlineTarget { .. } => todo!(),
1207            Self::InterpretedText { .. } => String::new(),
1208            Self::Label { .. } => todo!(),
1209            Self::Legend { .. } => "\n".to_string(),
1210            Self::Line { .. } => "\n".to_string(),
1211            Self::LineBlock { .. } => "\n".to_string(),
1212            Self::ListTable { .. } => "\\end{tabular}\n\n".to_string(),
1213            Self::Literal { .. } => "".to_string(),
1214            Self::LiteralBlock { .. } => "\n\\end{codeblock}\n\n".to_string(),
1215            Self::Math { .. } => "".to_string(),
1216            Self::MathBlock { .. } => "\\end{equation}\n\n".to_string(),
1217            Self::OptionList { .. } => "\n".to_string(),
1218            Self::OptionListItem { .. } => "\n".to_string(),
1219            Self::OptionString { .. } => todo!(),
1220            Self::Organization { .. } => todo!(),
1221            Self::Paragraph { .. } => "\n\n".to_string(),
1222            Self::ParsedLiteralBlock { .. } => "\n\n".to_string(),
1223            Self::Pending { .. } => todo!(),
1224            Self::Problematic { .. } => todo!(),
1225            Self::Raw { .. } => "\\end{raw}\n\n".to_string(),
1226            Self::Reference { .. } => "".to_string(),
1227            Self::Revision { .. } => todo!(),
1228            Self::Row { .. } => todo!(),
1229            Self::Rubric { .. } => "\n".to_string(),
1230            Self::Section { .. } => "".to_string(),
1231            Self::Sidebar { .. } => "\n".to_string(),
1232            Self::Status { .. } => todo!(),
1233            Self::StrongEmphasis { .. } => "".to_string(),
1234            Self::Subscript { .. } => "".to_string(),
1235            Self::SubstitutionDefinition { .. } => "\n".to_string(),
1236            Self::SubstitutionReference { .. } => "".to_string(),
1237            Self::Subtitle { .. } => "".to_string(),
1238            Self::Superscript { .. } => "".to_string(),
1239            Self::SystemMessage { .. } => todo!(),
1240            Self::Table { .. } => "\n".to_string(),
1241            Self::Target { .. } => "\n".to_string(),
1242            Self::TBody { .. } => "\n".to_string(),
1243            Self::Term { .. } => todo!(),
1244            Self::Text { .. } => "".to_string(),
1245            Self::TGroup { .. } => todo!(),
1246            Self::THead { .. } => "\n".to_string(),
1247            Self::TRow => "\\\\\n".to_string(),
1248            Self::Title { .. } => todo!(),
1249            Self::TitleReference { .. } => "".to_string(),
1250            Self::Topic { .. } => todo!(),
1251            Self::Transition { .. } => "\n".to_string(),
1252            Self::UnknownDirective { directive_name, .. } => {
1253                format!("\\end{{{}}}\n\n", directive_name.to_lowercase())
1254            }
1255            Self::Version { .. } => todo!(),
1256            Self::WhiteSpace { .. } => "".to_string(),
1257
1258            // ============================
1259            //  Sphinx specific directives
1260            // ============================
1261            Self::SphinxOnly {
1262                expression,
1263                body_indent,
1264            } => "\\end{only}\n\n".to_string(),
1265            Self::SphinxCodeBlock { .. } => String::from("\\end{codeblock}\n\n"),
1266
1267            // ========================
1268            //  A+ specific directives
1269            // ========================
1270            Self::AplusPOI { .. } => "\\end{poi}\n\n".to_string(),
1271            Self::AplusColBreak => "".to_string(),
1272            Self::AplusQuestionnaire { .. } => "\\end{quiz}\n\n".to_string(),
1273            Self::AplusPickOne { .. } => "\\end{pick}\n\n".to_string(),
1274            Self::AplusPickAny { .. } => "\\end{pick}\n\n".to_string(),
1275            Self::AplusFreeText { .. } => "\\end{freetext}\n\n".to_string(),
1276            Self::AplusPickChoices { .. } => "\\end{answers}\n\n".to_string(),
1277            Self::AplusPickChoice { label, .. } => {
1278                let label = format!(" \\label{{{}}}", label);
1279                format!("{}\n", label)
1280            }
1281            Self::AplusQuestionnaireHints { .. } => "\n".to_string(),
1282            Self::AplusQuestionnaireHint { .. } => "}\n".to_string(),
1283            Self::AplusSubmit { .. } => "\\end{submit}\n\n".to_string(),
1284            Self::AplusActiveElementInput { .. } => "\n\n".to_string(),
1285            Self::AplusActiveElementOutput { .. } => "\n\n".to_string(),
1286        };
1287
1288        post_string
1289    }
1290
1291    /// Generates a suitable reference anchor string per TreeNodeType.
1292    fn anchor_string (
1293        &self,
1294        refnames_from_node: Option<&Vec<String>>,
1295    ) -> String {
1296
1297        let mut anchor_string = String::new();
1298
1299        let (refname, anchor_type_str): (Option<&String>, &str) = match self {
1300            Self::Abbreviation { .. } => (None, ""),
1301            Self::AbsoluteURI { .. } => (None, ""),
1302            Self::Acronym { .. } => (None, ""),
1303            Self::Address => (None, ""),
1304            Self::Admonition { variant, .. } => (None, ""),
1305            Self::Attribution { .. } => (None, ""),
1306            Self::Author { .. } => (None, ""),
1307            Self::Authors { .. } => (None, ""),
1308            Self::AutomaticSectionNumbering { .. } => (None, ""),
1309            Self::BlockQuote { .. } => (None, ""),
1310            Self::BulletList { .. } => (None, ""),
1311            Self::BulletListItem { .. } => (None, ""),
1312            Self::Caption { .. } => (None, ""),
1313            Self::Citation { .. } => (None, ""),
1314            Self::CitationReference { .. } => (None, ""),
1315            Self::Class { .. } => (None, ""),
1316            Self::Classifier { .. } => (None, ""),
1317            Self::Code { .. } => (None, ""),
1318            Self::ColSpec { .. } => (None, ""),
1319            Self::Comment { .. } => (None, ""),
1320            Self::CompoundParagraph { .. } => (None, ""),
1321            Self::Contact { .. } => (None, ""),
1322            Self::Container { .. } => (None, ""),
1323            Self::Copyright { .. } => (None, ""),
1324            Self::CSVTable { .. } => (None, ""),
1325            Self::Date => (None, ""),
1326            Self::Decoration => (None, ""),
1327            Self::Definition => (None, ""),
1328            Self::DefinitionList { .. } => (None, ""),
1329            Self::DefinitionListItem { .. } => (None, ""),
1330            Self::Description => (None, ""),
1331            Self::DocInfo => (None, ""),
1332            Self::DoctestBlock { .. } => (None, ""),
1333            Self::Document { .. } => (None, ""),
1334            Self::Emphasis { .. } => (None, ""),
1335            Self::EmptyLine => (None, ""),
1336            Self::Entry { is_last } => (None, ""),
1337            Self::EnumeratedList { .. } => (None, ""),
1338            Self::EnumeratedListItem { .. } => (None, ""),
1339            Self::ExternalHyperlinkTarget { .. } => (None, ""),
1340            Self::Field => (None, ""),
1341            Self::FieldBody { .. } => (None, ""),
1342            Self::FieldList { .. } => (None, ""),
1343            Self::FieldListItem { .. } => (None, ""),
1344            Self::Figure { name, .. } => (name.as_ref(), "label"),
1345            Self::Footer { .. } => (None, ""),
1346            Self::Footnote { .. } => (None, ""),
1347            Self::FootnoteReference { .. } => (None, ""),
1348            Self::Header { .. } => (None, ""),
1349            Self::Generated => (None, ""),
1350            Self::Image { .. } => (None, ""),
1351            Self::Include { .. } => (None, ""),
1352            Self::IndirectHyperlinkTarget { .. } => (None, ""),
1353            Self::Inline { .. } => (None, ""),
1354            Self::InlineTarget { .. } => (None, ""),
1355            Self::InterpretedText { .. } => (None, ""),
1356            Self::Label { .. } => (None, ""),
1357            Self::Legend { .. } => (None, ""),
1358            Self::Line { .. } => (None, ""),
1359            Self::LineBlock { .. } => (None, ""),
1360            Self::ListTable { .. } => (None, ""),
1361            Self::Literal { .. } => (None, ""),
1362            Self::LiteralBlock { .. } => (None, ""),
1363            Self::Math { .. } => (None, ""),
1364            Self::MathBlock { name, .. } => (name.as_ref(), "rstlabel"),
1365            Self::OptionList { .. } => (None, ""),
1366            Self::OptionListItem { .. } => (None, ""),
1367            Self::OptionString { .. } => (None, ""),
1368            Self::Organization { .. } => (None, ""),
1369            Self::Paragraph { .. } => (None, ""),
1370            Self::ParsedLiteralBlock { .. } => (None, ""),
1371            Self::Pending { .. } => (None, ""),
1372            Self::Problematic { .. } => (None, ""),
1373            Self::Raw { .. } => (None, ""),
1374            Self::Reference { .. } => (None, ""),
1375            Self::Revision { .. } => (None, ""),
1376            Self::Row { .. } => (None, ""),
1377            Self::Rubric { .. } => (None, ""),
1378            Self::Section { .. } => (None, "rstlabel"),
1379            Self::Sidebar { .. } => (None, ""),
1380            Self::Status { .. } => (None, ""),
1381            Self::StrongEmphasis { .. } => (None, ""),
1382            Self::Subscript { .. } => (None, ""),
1383            Self::SubstitutionDefinition { .. } => (None, ""),
1384            Self::SubstitutionReference { .. } => (None, ""),
1385            Self::Subtitle { .. } => (None, ""),
1386            Self::Superscript { .. } => (None, ""),
1387            Self::SystemMessage { .. } => (None, ""),
1388            Self::Table { .. } => (None, ""),
1389            Self::Target { .. } => (None, ""),
1390            Self::TBody { .. } => (None, ""),
1391            Self::Term { .. } => (None, ""),
1392            Self::Text { .. } => (None, ""),
1393            Self::TGroup { .. } => (None, ""),
1394            Self::THead { .. } => (None, ""),
1395            Self::TRow => (None, ""),
1396            Self::Title { .. } => (None, ""),
1397            Self::TitleReference { .. } => (None, ""),
1398            Self::Topic { .. } => (None, ""),
1399            Self::Transition { .. } => (None, ""),
1400            Self::UnknownDirective { directive_name, .. } => (None, ""),
1401            Self::Version { .. } => (None, ""),
1402            Self::WhiteSpace { .. } => (None, ""),
1403
1404            // ============================
1405            //  Sphinx specific directives
1406            // ============================
1407            Self::SphinxOnly { .. } => (None, ""),
1408            Self::SphinxCodeBlock { .. } => (None, ""),
1409
1410            // ========================
1411            //  A+ specific directives
1412            // ========================
1413            Self::AplusPOI { .. } => (None, ""),
1414            Self::AplusColBreak => (None, ""),
1415            Self::AplusQuestionnaire { .. } => (None, ""),
1416            Self::AplusPickOne { .. } => (None, ""),
1417            Self::AplusPickAny { .. } => (None, ""),
1418            Self::AplusFreeText { .. } => (None, ""),
1419            Self::AplusPickChoices { .. } => (None, ""),
1420            Self::AplusPickChoice { label, .. } => (None, ""),
1421            Self::AplusQuestionnaireHints { .. } => (None, ""),
1422            Self::AplusQuestionnaireHint { .. } => (None, ""),
1423            Self::AplusSubmit { .. } => (None, ""),
1424            Self::AplusActiveElementInput { .. } => (None, ""),
1425            Self::AplusActiveElementOutput { .. } => (None, ""),
1426        };
1427
1428        // TODO get ridi of ths first block by having directive options add the refnames and HTML classes
1429        // to the directive node via DocTree::push_to_internal_target_stack, and not the contained data.
1430        // It is stupid to have the storage in two different places.
1431        if let Some(name) = refname {
1432            match anchor_type_str {
1433                "rstlabel" => {
1434                    anchor_string += &format!("\\{}{{{}}}\n", anchor_type_str, name);
1435                }
1436                "label" => {
1437                    anchor_string += &format!("\\{}{{{}}}\n", anchor_type_str, name);
1438                }
1439                "hypertarget" => {
1440                    anchor_string += &format!("\\{}{{{}}}{{{}}}\n", anchor_type_str, name, name);
1441                }
1442                "" => {
1443                    // Do nothing
1444                }
1445                _ => unreachable!("No anchor of type {}. Computer says no...", anchor_type_str)
1446            }
1447        }
1448        if let Some(names) = refnames_from_node {
1449            for name in names {
1450                match anchor_type_str {
1451                    "rstlabel" => {
1452                        anchor_string += &format!("\\{}{{{}}}\n", anchor_type_str, name);
1453                    }
1454                    "label" => {
1455                        anchor_string += &format!("\\{}{{{}}}\n", anchor_type_str, name);
1456                    }
1457                    "hypertarget" => {
1458                        anchor_string += &format!("\\{}{{{}}}{{{}}}\n", anchor_type_str, name, name);
1459                    }
1460                    "" => {
1461                        // Do nothing
1462                    }
1463                    _ => unreachable!("No anchor of type {}. Computer says no...", anchor_type_str)
1464                }
1465            }
1466        }
1467
1468        anchor_string
1469    }
1470}
1471
1472// =========
1473//  HELPERS
1474// =========
1475
1476/// Returns the contents of the LaTeX class file required by Larst projects
1477/// being compiled by `pdflatex` or `lualatex` as a `&'static str`.
1478/// The string was authored by Tomi Janhunen.
1479///
1480/// source: https://course-gitlab.tuni.fi/ITC/CS/larst/larstprod/-/raw/master/LarST-example/aplus.cls
1481/// url-date: 2020-09-17
1482fn aplus_cls_contents() -> &'static str {
1483    r#"%
1484% The LaRST Project
1485%
1486% alpus -- Documentclass for the direct LaTeX compilation of A+ materials
1487%
1488% (c) 2019-2020 Tomi Janhunen
1489
1490\NeedsTeXFormat{LaTeX2e}
1491\ProvidesClass{aplus}
1492
1493\LoadClass{book}
1494\RequirePackage{url}
1495\RequirePackage{graphicx}
1496\RequirePackage[breakable,most]{tcolorbox}
1497\RequirePackage{amsmath}
1498\RequirePackage{amssymb}
1499\RequirePackage{pifont}
1500\RequirePackage{keyval}
1501\RequirePackage{ifthen}
1502\RequirePackage{xstring}
1503\RequirePackage{comment}
1504\RequirePackage{environ}
1505\RequirePackage{fancyvrb}
1506\RequirePackage{hyperref}
1507
1508% Font issues
1509\RequirePackage[T1]{fontenc}
1510
1511% Reset page dimensions
1512\usepackage[nohead,nofoot,top=1in,margin=1in]{geometry}
1513\pagestyle{empty}
1514
1515% \newcommand{\chapter}[1]{{\Huge\textbf{#1}}}
1516
1517% Set fonts toward ``Read the Docs''
1518\usepackage[scaled]{helvet}
1519\renewcommand\familydefault{\sfdefault}
1520
1521% No indentation
1522\setlength{\parindent}{0pt}
1523\setlength{\parskip}{0.5\baselineskip}
1524
1525% Remove (sub)section numbering
1526% \makeatletter
1527% \renewcommand{\@seccntformat}[1]{}
1528% \makeatother
1529
1530% Unification of labels
1531\global\def\labelhere{}
1532\newcommand{\rstlabel}[1]{\global\def\labelhere{\hypertarget{#1}{}\label{#1}}}
1533
1534% RST Simulations in LaTeX
1535
1536\newcommand{\aplus}[2]{}
1537
1538\makeatletter
1539\long\def\notext#1{}
1540\newenvironment{only}[1][foo]{%
1541  \ifthenelse{\equal{#1}{latex}}%
1542  {}{\Collect@Body\notext}
1543  }{}
1544\makeatother
1545
1546\newenvironment{raw}{}{}
1547\RenewEnviron{raw}{}{}
1548
1549\newcommand{\code}[1]{\texttt{#1}}
1550
1551% Blocks of code
1552
1553\makeatletter
1554\define@key{codeblock}{python}[]{}
1555\makeatother
1556
1557\newcommand\innercodeblock[1][]{#1}
1558\newenvironment{codeblock}{ \bgroup\verbatim\innercodeblock }{ \endverbatim\egroup }
1559% \newenvironment{codeblock}[1][]{\begin{BVerbatim}}{\end{BVerbatim}}
1560
1561% File download
1562
1563\newcommand{\download}[2]{\par\texttt{#1}\footnote{\url{#2}}}
1564\newcommand{\rstclass}[1]{}
1565\newcommand{\feedback}[2]{\par\textbf{#1}. #2 \\}
1566
1567\newenvironment{toggle}[1]{\textbf{#1}. }{}
1568
1569
1570% Points of interest (slide-type objects within material)
1571
1572\makeatletter
1573\define@key{poi}{hidden}[]{}
1574\define@key{poi}{columns}[]{\def\poi@colums{#1}}
1575\define@key{poi}{id}[]{\def\poi@id{#1}}
1576\define@key{poi}{next}[]{\def\poi@next{#1}}
1577\define@key{poi}{prev}[]{\def\poi@prev{#1}}
1578\define@key{poi}{bgimg}[]{\def\poi@bgimg{#1}}
1579\makeatother
1580
1581\newcommand{\newcol}{\newpage} % Semantic mismatch
1582\newenvironment{poi}[2][]{%
1583\setkeys{poi}{#1}
1584\par\noindent\begin{large}\begin{tcolorbox}[width=\textwidth,adjusted title=#2]%
1585}{%
1586\end{tcolorbox}\end{large}}
1587
1588% Active elements
1589
1590\makeatletter
1591\newlength{\ae@width}
1592\newlength{\ae@height}
1593\define@key{aelement}{width}[]{\def\ae@width{#1}}
1594\define@key{aelement}{height}[]{\def\ae@height{#1}}
1595\define@key{aelement}{class}[]{\def\ae@class{#1}}
1596\define@key{aelement}{type}[]{\def\ae@type{#1}}
1597\setkeys{aelement}{width=\textwidth,height=\baselineskip,type=pdf,class=left}%
1598\newcommand{\aeinput}[2][]{\setkeys{aelement}{#1}}
1599\newcommand{\aeoutput}[3][]{\setkeys{aelement}{#1}}
1600\makeatother
1601
1602% Submission fields
1603
1604\makeatletter
1605\define@key{submit}{config}[]{\def\sbm@config{#1}}
1606\define@key{submit}{submissions}[]{\def\sbm@submissions{#1}}
1607\define@key{submit}{points-to-pass}[]{\def\sbm@ptp{#1}}
1608\define@key{submit}{class}[]{\def\sbm@class{#1}}
1609\define@key{submit}{title}[]{\def\sbm@title{#1}}
1610\define@key{submit}{category}[]{\def\sbm@category{#1}}
1611\define@key{submit}{status}[]{\def\sbm@status{#1}}
1612\define@key{submit}{allow-assistant-viewing}[]{\def\sbm@viewing{#1}}
1613\define@key{submit}{allow-assistant-grading}[]{\def\sbm@grading{#1}}
1614\define@key{submit}{url}[]{\def\sbm@url{#1}}
1615\define@key{submit}{lti}[]{\def\sbm@lti{#1}}
1616\define@key{submit}{ajax}[]{\def\sbm@ajax{true}}
1617\define@key{submit}{quiz}[]{\def\sbm@quiz{true}}
1618\makeatother
1619
1620\newenvironment{submit}[2][]{%
1621\setkeys{submit}{#1}%
1622\par\noindent\begin{tcolorbox}[width=\textwidth,adjusted title=#2]%
1623}{%
1624\end{tcolorbox}}
1625
1626% Quizzes
1627
1628\newcommand{\wrong}{\item[\fbox{\phantom{\large x}}]}
1629\renewcommand{\right}{\item[\fbox{\large x}]}
1630\newcommand{\undet}{\item[\fbox{\large *}]}
1631
1632\newcounter{question}\stepcounter{question}
1633\newenvironment{answers}{\begin{enumerate}}{\end{enumerate}}
1634
1635\makeatletter
1636\define@key{quiz}{submissions}[]{\def\qz@submissions{#1}}
1637\define@key{quiz}{points-to-pass}[]{\def\qz@points{#1}}
1638\define@key{quiz}{title}[]{\def\qz@title{#1}}
1639\define@key{quiz}{pick-randomly}[]{\def\qz@randomly{#1}}
1640\define@key{quiz}{category}[]{\def\qz@category{#1}}
1641\define@key{quiz}{status}[]{\def\qz@status{#1}}
1642\define@key{quiz}{reveal-model-at-max-submissions}[]{\def\qz@reveal{#1}}
1643\define@key{quiz}{show-model}[]{\def\qz@show{#1}}
1644\define@key{quiz}{allow-assistant-viewing}[]{\def\qz@viewing{#1}}
1645\define@key{quiz}{allow-assistant-grading}[]{\def\qz@grading{#1}}
1646\define@key{quiz}{feedback}[]{\def\qz@feedback{true}}
1647\define@key{quiz}{no-override}[]{\def\qz@noover{true}}
1648\define@key{quiz}{preserve-questions-between-attempts}[]{\def\qz@preserve{true}}
1649\setkeys{quiz}{}%
1650\newenvironment{quiz}[3][]{%
1651\setkeys{quiz}{#1}{}%
1652\section*{Quiz #2}}{\setcounter{question}{1}}
1653\makeatother
1654
1655% Pick
1656
1657\makeatletter
1658\define@key{pick}{class}[]{\def\pick@class{#1}}
1659\define@key{pick}{key}[]{\def\pick@key{#1}}
1660\define@key{pick}{randomized}[]{\def\pick@randomized{#1}}
1661\define@key{pick}{correct-count}[]{\def\pick@correct{#1}}
1662\define@key{pick}{required}[]{\def\pick@required{true}}
1663\define@key{pick}{partial-points}[]{\def\pick@partial{true}}
1664\setkeys{pick}{}%
1665\newenvironment{pick}[3][]{%
1666\setkeys{pick}{#1}{}%
1667\par\textbf{Q\thequestion:}~}{\stepcounter{question}}
1668\makeatother
1669
1670% Freetext
1671
1672\makeatletter
1673\newlength{\ft@height}
1674\newlength{\ft@length}
1675\define@key{freetext}{required}[]{\def\ft@required{true}}
1676\define@key{freetext}{length}[]{\def\ft@length{#1}}
1677\define@key{freetext}{height}[]{\def\ft@height{#1}}
1678\define@key{freetext}{class}[]{\def\ft@class{#1}}
1679\define@key{freetext}{key}[]{\def\ft@key{#1}}
1680\setkeys{freetext}{length=100em,height=5\baselineskip,class=left}%
1681\newenvironment{freetext}[4][]{%
1682\setkeys{freetext}{#1}{}
1683\par\textbf{Q\thequestion:}~}{\stepcounter{question}}
1684\makeatother
1685
1686% LaTeX environments (assumed by default, some used in limited ways)
1687
1688% \begin{document} ... \end{document}
1689% \begin{itemize} ... \item ... \end{itemize}
1690% \begin{enumerate} ... \item ... \end{enumerate}
1691% \begin{tabular}[...] ... & ... & ... \\ ... \end{tabular}
1692% \begin{thebibliography}{...} ... \end{thebibligraphy}
1693% \begin{equation} ... \end{equation}
1694% \begin{center} ... \end{center}
1695
1696% LaTeX commands (assumed by default)
1697
1698% \documentclass{}
1699% \bibliographystyle{...}
1700% \tableofcontents
1701% \contentsline{...}{...}{...}
1702% \chapter{...}
1703% \section{...}
1704% \subsection{...}
1705% \emph{...} or {\em ...}
1706% \textit{...}
1707% \textbf{...} or {\bf ...}
1708% \texttt{...}
1709% \captionof{...}{...}
1710% \newcounter{...}
1711% \the...
1712% \stepcounter{...}
1713% \refstepcounter{...}
1714% \addtocounter{...}{...}
1715% \setcounter{...}{...}
1716% \numberwithin{...}{...}
1717% \include{...}
1718% \input{...}
1719% \includegraphics[...]{...}
1720% \cite{...}
1721% \ref{...}
1722% \label{...}
1723% \url{...}
1724% \href{...}{...}
1725% \hyperref[...]{...}
1726% \hypertarget{...}{...}
1727% \hyperlink{...}{...}
1728% \textbackslash
1729% \textasciicircum
1730% \textunderscore
1731% \textasciitilde
1732% \nbspc
1733% \aa
1734% \AA
1735% \hrulefill
1736
1737"#
1738}
1739
1740/// rst_length_to_string
1741///
1742/// Converts a given reStructuredText length reference into a string.
1743fn rst_length_to_string(length: &Length) -> String {
1744    match length {
1745        Length::Em(val) => val.to_string() + "em",
1746        Length::Ex(val) => val.to_string() + "ex",
1747        Length::Mm(val) => val.to_string() + "mm",
1748        Length::Cm(val) => val.to_string() + "cm",
1749        Length::In(val) => val.to_string() + "in",
1750        Length::Px(val) => val.to_string() + "px",
1751        Length::Pt(val) => val.to_string() + "pt",
1752        Length::Pc(val) => val.to_string() + "pc",
1753    }
1754}
1755
1756/// ### html_alignment_to_string
1757///
1758/// Converts a HTMLAlignment variant to the corresponding string.
1759fn html_alignment_to_string(alignment: &HTMLAlignment) -> String {
1760    match alignment {
1761        HTMLAlignment::Top => String::from("top"),
1762        HTMLAlignment::Middle => String::from("middle"),
1763        HTMLAlignment::Bottom => String::from("bottom"),
1764        HTMLAlignment::Left => String::from("left"),
1765        HTMLAlignment::Center => String::from("center"),
1766        HTMLAlignment::Right => String::from("right"),
1767    }
1768}
1769
1770/// ### horizontal_alignment_to_string
1771///
1772/// Converts a HorizontalAlignment variant to the corresponding string.
1773fn horizontal_alignment_to_string(alignment: &HorizontalAlignment) -> String {
1774    match alignment {
1775        HorizontalAlignment::Left => String::from("left"),
1776        HorizontalAlignment::Center => String::from("center"),
1777        HorizontalAlignment::Right => String::from("right"),
1778    }
1779}
1780
1781/// Generates a single `rstlabel` string form a given optional vector of strings.
1782/// The generated string is empty, if `None` is given or the label vector is empty.
1783fn rstlabel_string_from_labels (labels: Option<&Vec<String>>) -> String {
1784
1785    if let Some(labels) = labels {
1786        let mut label_string = String::new();
1787        for label in labels {
1788            let rst_label = format!("\\rstlabel{{{}}}\n", label);
1789            label_string.push_str(&rst_label)
1790        }
1791        label_string
1792    } else {
1793        String::new()
1794    }
1795}
1796
1797/// Generates a single `rstlabel` string form a given optional vector of strings.
1798/// The generated string is empty, if `None` is given or the label vector is empty.
1799fn latex_label_string_from_labels (labels: Option<&Vec<String>>) -> String {
1800
1801    if let Some(labels) = labels {
1802        let mut label_string = String::new();
1803        for label in labels {
1804            let rst_label = format!("\\label{{{}}}\n", label);
1805            label_string.push_str(&rst_label)
1806        }
1807        label_string
1808    } else {
1809        String::new()
1810    }
1811}