rustla/parser/
directive_parsers.rs

1/*!
2A file that contains functions dedicated to
3parsing *directives*, reStructuredText extensions.
4All of these follow the same basic pattern:
5
61. check for directive argument
72. check for directive options and filter them for accepted ones,
83. return with a transition result, so that the `parser` might continue
9    parsing the contents of the directive node.
10
11Some small deviations from this pattern might occur,
12since a directive might not accept arguments or options,
13or just not have any content.
14
15Copyright © 2020 Santtu Söderholm
16*/
17use std::collections::HashMap;
18
19use crate::common::ParsingResult;
20use crate::doctree::tree_node_types::TreeNodeType;
21use crate::doctree::DocTree;
22use crate::parser::line_cursor::LineCursor;
23use crate::parser::state_machine::State;
24use crate::parser::types_and_aliases::{
25    InlineParsingResult, LineAdvance, PushOrPop, TransitionResult,
26};
27use crate::parser::Parser;
28use crate::parser::converters;
29use crate::parser::types_and_aliases::IndentedBlockResult;
30use crate::parser::types_and_aliases::TextBlockResult;
31
32pub fn parse_standard_admonition(
33    src_lines: &Vec<String>,
34    body_indent: usize,
35    section_level: usize,
36    first_indent: usize,
37    mut doctree: DocTree,
38    line_cursor: &mut LineCursor,
39    admonition_type: &str,
40    empty_after_marker: bool,
41) -> TransitionResult {
42    use crate::doctree::directives::AdmonitionType;
43
44    let variant: AdmonitionType = match admonition_type {
45        "attention" => AdmonitionType::Attention,
46        "caution" => AdmonitionType::Caution,
47        "danger" => AdmonitionType::Danger,
48        "error" => AdmonitionType::Error,
49        "hint" => AdmonitionType::Hint,
50        "important" => AdmonitionType::Important,
51        "note" => AdmonitionType::Note,
52        "tip" => AdmonitionType::Tip,
53        "warning" => AdmonitionType::Warning,
54        _ => unreachable!(
55            "No standard admonition type \"{}\" on line {}. Computer says no...",
56            admonition_type,
57            line_cursor.sum_total()
58        ),
59    };
60
61    let mut arg_lines = if let Some(arg) = scan_directive_arguments(
62        src_lines,
63        line_cursor,
64        body_indent,
65        Some(first_indent),
66        empty_after_marker,
67    ) {
68        arg
69    } else {
70        Vec::new()
71    };
72
73    // Try scanning for options, if first block was empty
74    let (classes, name) = if let Some(mut options) =
75        scan_directive_options(src_lines, line_cursor, body_indent)
76    {
77        (
78            options.remove("class"),
79            options.remove("name"),
80        )
81    } else {
82        (None, None)
83    };
84
85    // Read in the rest of the admonition contents...
86    let offset = match Parser::read_indented_block(
87        src_lines,
88        line_cursor.relative_offset(),
89        false,
90        true,
91        Some(body_indent),
92        Some(body_indent),
93        false) {
94            IndentedBlockResult::Ok {mut lines, minimum_indent, offset, blank_finish } => {
95            arg_lines.append(&mut lines);
96            offset
97        },
98        _ => return TransitionResult::Failure {
99            message: format!("Error when reading in the contents of \"{}\" around line {}. Computer says no...", variant.to_string(), line_cursor.sum_total()),
100            doctree: doctree
101        }
102    };
103
104    // Create admonition node...
105    let admonition_data = TreeNodeType::Admonition {
106        content_indent: body_indent,
107        classes: classes,
108        name: name,
109        variant: variant.clone(),
110    };
111
112    // Focus on the created node...
113    doctree = match doctree.push_data_and_focus(admonition_data) {
114        Ok(tree) => tree,
115        Err(tree) => {
116            return TransitionResult::Failure {
117                message: format!(
118                    "Node insertion error on line {}. Computer says no...",
119                    line_cursor.sum_total()
120                ),
121                doctree: tree,
122            }
123        }
124    };
125
126    // Start nested parse inside admonition...
127    let (doctree, nested_state_stack) = match Parser::new(
128        arg_lines,
129        doctree,
130        body_indent,
131        line_cursor.sum_total(),
132        State::Admonition,
133        section_level,
134    ).parse() {
135        ParsingResult::EOF {
136            doctree,
137            state_stack,
138        } => (doctree, state_stack),
139        ParsingResult::EmptyStateStack {
140            doctree,
141            state_stack,
142        } => (doctree, state_stack),
143        ParsingResult::Failure { message, doctree } => {
144            return TransitionResult::Failure {
145                message: format!(
146                    "Error when parsing a \"{}\" on line {}: {}",
147                    variant,
148                    line_cursor.sum_total(),
149                    message
150                ),
151                doctree: doctree,
152            }
153        }
154    };
155
156    TransitionResult::Success {
157        doctree: doctree,
158        push_or_pop: PushOrPop::Push(nested_state_stack),
159        line_advance: LineAdvance::Some(offset),
160    }
161}
162
163/// Much like `parse_standard_admonition`, except
164/// 1. first checks that the admonition contains an argument,
165/// 2. then checks for possible options and
166/// 3. focuses on the admonition itself.
167pub fn parse_generic_admonition(
168    src_lines: &Vec<String>,
169    mut doctree: DocTree,
170    line_cursor: &mut LineCursor,
171    empty_after_marker: bool,
172    body_indent: usize,
173    first_indent: Option<usize>,
174) -> TransitionResult {
175
176    let argument = if let Some(arg) =
177        scan_directive_arguments(src_lines, line_cursor, body_indent, first_indent, empty_after_marker)
178    {
179        arg
180    } else {
181        return TransitionResult::Failure {
182            message: format!("General admonition on line {} does not contain a compulsory title argument. Computer says no...", line_cursor.sum_total()),
183            doctree: doctree
184        };
185    };
186
187    let directive_options =
188        scan_directive_options(src_lines, line_cursor, body_indent);
189
190    let (classes, name) = if let Some(mut options) = directive_options {
191        (
192            options.remove("class"),
193            options.remove("name")
194        )
195    } else {
196        (None, None)
197    };
198
199    let admonition_data = TreeNodeType::Admonition {
200        content_indent: body_indent,
201        classes: classes,
202        name: name,
203        variant: crate::doctree::directives::AdmonitionType::Admonition {
204            title: argument.join(" "),
205        },
206    };
207
208    doctree = match doctree.push_data_and_focus(admonition_data) {
209        Ok(tree) => tree,
210        Err(tree) => {
211            return TransitionResult::Failure {
212                message: format!(
213                    "Node insertion error on line {}. Computer says no...",
214                    line_cursor.sum_total()
215                ),
216                doctree: tree,
217            }
218        }
219    };
220
221    TransitionResult::Success {
222        doctree: doctree,
223        push_or_pop: PushOrPop::Push(vec![State::Admonition]),
224        line_advance: LineAdvance::None,
225    }
226}
227
228pub fn parse_image(
229    src_lines: &Vec<String>,
230    mut doctree: DocTree,
231    line_cursor: &mut LineCursor,
232    empty_after_marker: bool,
233    body_indent: usize,
234    first_indent: Option<usize>,
235) -> TransitionResult {
236
237    let argument = if let Some(arg) =
238        scan_directive_arguments(src_lines, line_cursor, body_indent, first_indent, empty_after_marker)
239    {
240        arg
241    } else {
242        return TransitionResult::Failure {
243            message: format!(
244                "Image on line {} does not contain a compulsory image URI. Computer says no...",
245                line_cursor.sum_total()
246            ),
247            doctree: doctree,
248        };
249    };
250
251    let (alt, height, width, scale, align, target, classes, name) =
252        if let Some(mut options) = scan_directive_options(src_lines, line_cursor, body_indent) {
253            (
254                options.remove("alt"),
255                options.remove("height"),
256                options.remove("width"),
257                options.remove("scale"),
258                options.remove("align"),
259                options.remove("target"),
260                options.remove("class"),
261                options.remove("name")
262            )
263        } else {
264            (None, None, None, None, None, None, None, None)
265        };
266
267    let image_data = TreeNodeType::Image {
268        uri: argument.join(""),
269        alt: alt,
270        height: if let Some(h) = &height {
271            converters::str_to_length(h)
272        } else {
273            None
274        },
275        width: if let Some(w) = &width {
276            converters::str_to_length(w)
277        } else {
278            None
279        },
280        scale: if let Some(scale) = &scale {
281            converters::str_to_percentage(scale)
282        } else {
283            None
284        },
285        align: if let Some(a) = &align {
286            converters::str_to_html_alignment(a)
287        } else {
288            None
289        },
290        target: target,
291        name: name,
292        class: classes,
293        inline: false
294    };
295
296    doctree = match doctree.push_data(image_data) {
297        Ok(tree) => tree,
298        Err(tree) => {
299            return TransitionResult::Failure {
300                message: format!(
301                    "Node insertion error on line {}. Computer says no...",
302                    line_cursor.sum_total()
303                ),
304                doctree: tree,
305            }
306        }
307    };
308
309    TransitionResult::Success {
310        doctree: doctree,
311        push_or_pop: PushOrPop::Neither,
312        line_advance: LineAdvance::None,
313    }
314}
315
316pub fn parse_figure(
317    src_lines: &Vec<String>,
318    mut doctree: DocTree,
319    line_cursor: &mut LineCursor,
320    base_indent: usize,
321    empty_after_marker: bool,
322    body_indent: usize,
323    first_indent: Option<usize>,
324    section_level: usize,
325) -> TransitionResult {
326    let argument = if let Some(arg) =
327        scan_directive_arguments(src_lines, line_cursor, body_indent, first_indent, empty_after_marker)
328    {
329        arg
330    } else {
331        return TransitionResult::Failure {
332    message: format!("Figure on line {} does not contain a compulsory image URI. Computer says no...", line_cursor.sum_total()),
333    doctree: doctree
334    };
335    };
336
337    let (alt, height, width, scale, align, target, classes, name, figwidth, figclass) =
338        if let Some(mut options) =
339            scan_directive_options(src_lines, line_cursor, body_indent)
340        {
341            (
342                options.remove("alt"),
343                options.remove("height"),
344                options.remove("width"),
345                options.remove("scale"),
346                options.remove("align"),
347                options.remove("target"),
348                options.remove("class"),
349                options.remove("name"),
350                options.remove("figwidth"),
351                options.remove("figclass")
352            )
353        } else {
354            (None, None, None, None, None, None, None, None, None, None)
355        };
356
357    // Construct the contained image
358    let image = TreeNodeType::Image {
359        uri: argument.join(""),
360
361        alt: alt,
362        height: if let Some(h) = height {
363            converters::str_to_length(&h)
364        } else {
365            None
366        },
367        width: if let Some(w) = width {
368            converters::str_to_length(&w)
369        } else {
370            None
371        },
372        scale: if let Some(scale) = &scale {
373            converters::str_to_percentage(scale)
374        } else {
375            None
376        },
377        align: None, // Image does not have alignenment inside a figure.
378        target: target,
379        class: classes,
380        name: None, // Sphinx patch moved "name" to containing figure node
381        inline: false
382    };
383
384    let figure = TreeNodeType::Figure {
385        body_indent: body_indent,
386        align: if let Some(a) = &align {
387            converters::str_to_horizontal_alignment(a)
388        } else {
389            None
390        },
391        figclass: figclass,
392        figwidth: if let Some(w) = &figwidth {
393            converters::str_to_length(w)
394        } else {
395            None
396        },
397        name: if let Some(refname) = &name {
398            Some(crate::common::normalize_refname(refname))
399        } else {
400            None
401        }
402    };
403
404    // Add figure node to tree and focus on it
405    doctree = match doctree.push_data_and_focus(figure) {
406        Ok(tree) => tree,
407        Err(tree) => {
408            return TransitionResult::Failure {
409                message: format!(
410                    "Node insertion error on line {}. Computer says no...",
411                    line_cursor.sum_total()
412                ),
413                doctree: tree,
414            }
415        }
416    };
417
418    // Add image to figure
419    doctree = match doctree.push_data(image) {
420        Ok(tree) => tree,
421        Err(tree) => {
422            return TransitionResult::Failure {
423                message: format!(
424                    "Node insertion error on line {}. Computer says no...",
425                    line_cursor.sum_total()
426                ),
427                doctree: tree,
428            }
429        }
430    };
431
432    TransitionResult::Success {
433        doctree: doctree,
434        push_or_pop: PushOrPop::Push(vec![State::Figure]),
435        line_advance: LineAdvance::None,
436    }
437}
438
439pub fn parse_topic() {
440    todo!()
441}
442
443pub fn parse_sidebar() {
444    todo!()
445}
446
447pub fn parse_line_block() {
448    todo!()
449}
450
451pub fn parse_parsed_literal() {
452    todo!()
453}
454
455/// The "code" directive parser.
456pub fn parse_code(
457    src_lines: &Vec<String>,
458    mut doctree: DocTree,
459    line_cursor: &mut LineCursor,
460    base_indent: usize,
461    empty_after_marker: bool,
462    body_indent: usize,
463    first_indent: Option<usize>,
464    section_level: usize,
465) -> TransitionResult {
466
467    let language = if let Some(arg) =
468        scan_directive_arguments(src_lines, line_cursor, body_indent, first_indent, empty_after_marker)
469    {
470        Some(arg.join(""))
471    } else {
472        None
473    };
474
475    let directive_options =
476        scan_directive_options(src_lines, line_cursor, body_indent);
477
478    let (classes, name, number_lines) = if let Some(mut options) = directive_options {
479        (
480            options.remove("class"),
481            options.remove("name"),
482            options.remove("number-lines"),
483        )
484    } else {
485        (None, None, None)
486    };
487
488    let (lines, offset) = if let IndentedBlockResult::Ok {lines, minimum_indent, offset, blank_finish } = Parser::read_indented_block(
489        src_lines,
490        line_cursor.relative_offset(),
491        false,
492        true,
493        Some(body_indent),
494        None,
495        false,
496    ) {
497        (lines, offset)
498    } else {
499        return TransitionResult::Failure {
500            message: format!(
501                "Could not read the code block on line {}. Computer says no...",
502                line_cursor.sum_total()
503            ),
504            doctree: doctree,
505        };
506    };
507
508    let code_block = TreeNodeType::Code {
509        text: lines.join("\n"),
510        language: language,
511        number_lines: number_lines,
512        class: classes,
513        name: name,
514    };
515
516    doctree = match doctree.push_data(code_block) {
517        Ok(tree) => tree,
518        Err(tree) => {
519            return TransitionResult::Failure {
520                message: format!(
521                    "Node insertion error on line {}. Computer says no...",
522                    line_cursor.sum_total()
523                ),
524                doctree: tree,
525            }
526        }
527    };
528
529    TransitionResult::Success {
530        doctree: doctree,
531        push_or_pop: PushOrPop::Neither,
532        line_advance: LineAdvance::Some(offset),
533    }
534}
535
536/// The display math parser. Content blocks separated by a blank lines are put in adjacent math blocks.
537pub fn parse_math_block(
538    src_lines: &Vec<String>,
539    mut doctree: DocTree,
540    line_cursor: &mut LineCursor,
541    body_indent: usize,
542    empty_after_marker: bool,
543    first_indent: usize,
544) -> TransitionResult {
545
546    let math_after_marker = scan_directive_arguments(
547        src_lines,
548        line_cursor,
549        body_indent,
550        Some(first_indent),
551        empty_after_marker
552    );
553
554    let (classes, name, nowrap, label) = if let Some(mut options)
555        = scan_directive_options(src_lines, line_cursor, body_indent)
556    {
557        (
558            options.remove("class"),
559            options.remove("name"),
560            // These were added by Sphinx
561            options.remove("nowrap"),
562            options.remove("label"),
563        )
564    } else {
565        (None, None, None, None)
566    };
567
568    // If an equation was given as an argument, quit early
569    if let Some(math) = math_after_marker {
570        doctree = match doctree.push_data(
571            TreeNodeType::MathBlock {
572                math_block: math.join("\n"),
573                class: classes,
574                name: name,
575            }
576        ) {
577            Ok(tree) => tree,
578            Err(tree) => {
579                return TransitionResult::Failure {
580                    message: format!(
581                        "Node insertion error on line {}. Computer says no...",
582                        line_cursor.sum_total()
583                    ),
584                    doctree: tree,
585                }
586            }
587        };
588        return TransitionResult::Success {
589            doctree: doctree,
590            push_or_pop: PushOrPop::Neither,
591            line_advance: LineAdvance::None,
592        };
593    }
594
595    // If no equation as argument, try reading block contents as multiple equations...
596    let (lines, offset) = match Parser::read_indented_block(
597        src_lines,
598        line_cursor.relative_offset(),
599        false,
600        true,
601        Some(body_indent),
602        Some(body_indent),
603        false
604    ) {
605        IndentedBlockResult::Ok {lines, minimum_indent, offset, blank_finish } => (lines, offset),
606        _ => return TransitionResult::Failure {
607            message: format!(
608                "Could not read the math block on line {}. Computer says no...",
609                line_cursor.sum_total()
610            ),
611            doctree: doctree,
612        }
613    };
614
615    // Scan lines for blocks separated by blank lines
616    let blocks = {
617        let mut blocks = Vec::new();
618        let mut block = String::new();
619
620        for line in lines.iter() {
621            if ! line.trim().is_empty() {
622                block = block + "\n" + line;
623            } else if line.trim().is_empty() && !block.trim().is_empty() {
624                blocks.push(block);
625                block = String::new();
626            } else {
627                continue
628            }
629        }
630
631        if ! block.trim().is_empty() {
632            blocks.push(block)
633        }
634
635        blocks
636    };
637
638    if blocks.is_empty() {
639        return TransitionResult::Failure {
640            message: format!("Tried reading a math block on line {} but didn't find any actual content. Computer says no...", line_cursor.sum_total()),
641            doctree: doctree
642        };
643    }
644
645    // A counter for separating refnames of multiple blocks generated by the same directive
646    let mut refname_counter = if blocks.len() == 1 {
647        None
648    } else {
649        Some(0)
650    };
651    for block in blocks {
652        doctree = match doctree.push_data(
653            TreeNodeType::MathBlock {
654                math_block: block.trim().to_string(),
655                name: if let Some(name) = &name {
656                    match &mut refname_counter {
657                        Some(counter) => {
658                            *counter += 1;
659                            Some(name.clone() + &(counter.to_string()))
660                        },
661                        None => Some(name.clone())
662                    }
663                } else {
664                    None
665                },
666                class: classes.clone(),
667            }
668        ) {
669            Ok(tree) => tree,
670            Err(tree) => {
671                return TransitionResult::Failure {
672                    message: format!(
673                        "Node insertion error on line {}. Computer says no...",
674                        line_cursor.sum_total()
675                    ),
676                    doctree: tree,
677                }
678            }
679        };
680    }
681
682    TransitionResult::Success {
683        doctree: doctree,
684        push_or_pop: PushOrPop::Neither,
685        line_advance: LineAdvance::Some(offset),
686    }
687}
688
689pub fn parse_rubric() {
690    todo!()
691}
692
693pub fn parse_epigraph() {
694    todo!()
695}
696
697pub fn parse_highlights() {
698    todo!()
699}
700
701pub fn parse_pull_quote() {
702    todo!()
703}
704
705pub fn parse_compound() {
706    todo!()
707}
708
709pub fn parse_container() {}
710
711pub fn parse_table() {
712    todo!()
713}
714
715pub fn parse_csv_table() {
716    todo!()
717}
718
719pub fn parse_list_table(
720    src_lines: &Vec<String>,
721    mut doctree: DocTree,
722    line_cursor: &mut LineCursor,
723    base_indent: usize,
724    empty_after_marker: bool,
725    first_indent: Option<usize>,
726    body_indent: usize,
727    section_level: usize,
728) -> TransitionResult {
729
730    let table_title = if let Some(title) = scan_directive_arguments(
731        src_lines,
732        line_cursor,
733        body_indent,
734        first_indent,
735        empty_after_marker,
736    ) {
737        title.join(" ")
738    } else {
739        String::new()
740    };
741
742    let (header_rows, stub_columns, width, widths, class, name, align) =
743        if let Some(mut options) = scan_directive_options
744        (src_lines, line_cursor, body_indent) {
745            (
746                options.remove("header-rows"),
747                options.remove("stub-columns"),
748                options.remove("width"),
749                options.remove("widths"),
750                options.remove("class"),
751                options.remove("name"),
752                options.remove("align"),
753            )
754        } else {
755            (None, None, None, None, None, None, None)
756        };
757
758    use crate::common::{HorizontalAlignment, MetricType, TableColWidths};
759
760    let list_table_node = TreeNodeType::ListTable {
761        body_indent: body_indent,
762
763        title: if !table_title.is_empty() {
764            Some(table_title)
765        } else {
766            None
767        },
768        widths: if let Some(widths) = widths {
769            if widths.as_str().trim() == "auto" {
770                Some(TableColWidths::Auto)
771            } else {
772                let widths = widths
773                    .split_whitespace()
774                    .filter(
775                        |s| ! s.is_empty()
776                    )
777                    .map(
778                        |int|
779                            if let Ok(result) = int.parse::<f64>() {
780                                result
781                            } else {
782                                panic!(
783                                    "Tried converting a list table column width \"{}\" into a integer on line {} but failed. Computer says no...",
784                                    int,
785                                    line_cursor.sum_total()
786                                );
787                            }
788                    )
789                    .collect::<Vec<f64>>();
790                if widths.len() == 0 {
791                    None
792                } else {
793                    Some(TableColWidths::Columns(widths))
794                }
795            }
796        } else {
797            None
798        },
799        width: if let Some(width) = width {
800            if let Some(length) = converters::str_to_length(&width) {
801                Some(MetricType::Lenght(length))
802            } else if let Some(percentage) = converters::str_to_percentage(&width) {
803                Some(crate::common::MetricType::Percentage(percentage))
804            } else {
805                None
806            }
807        } else {
808            None
809        },
810        header_rows: if let Some(num) = header_rows {
811            if let Ok(result) = num.parse::<u32>() {
812                Some(result)
813            } else {
814                eprintln!(
815                    "Could not parse list-table header-rows setting to integer on line {}...",
816                    line_cursor.sum_total()
817                );
818                None
819            }
820        } else {
821            None
822        },
823        stub_columns: if let Some(num) = stub_columns {
824            if let Ok(result) = num.parse::<u32>() {
825                Some(result)
826            } else {
827                eprintln!(
828                    "Could not parse list-table stub-columns setting to integer on line {}...",
829                    line_cursor.sum_total()
830                );
831                None
832            }
833        } else {
834            None
835        },
836        align: if let Some(alignment) = align {
837            match alignment.as_str() {
838                "left" => Some(HorizontalAlignment::Left),
839                "center" => Some(HorizontalAlignment::Center),
840                "right" => Some(HorizontalAlignment::Right),
841                _ => {
842                    eprintln!("Found an alignment setting for list table on line {}, but setting not valid...", line_cursor.sum_total());
843                    None
844                }
845            }
846        } else {
847            None
848        },
849    };
850
851    Parser::skip_empty_lines(src_lines, line_cursor);
852
853    doctree = match doctree.push_data_and_focus(list_table_node) {
854        Ok(tree) => tree,
855        Err(tree) => {
856            return TransitionResult::Failure {
857                message: format!(
858                    "Node insertion error on line {}. Computer says no...",
859                    line_cursor.sum_total()
860                ),
861                doctree: tree,
862            }
863        }
864    };
865
866    let (lines, offset) = if let IndentedBlockResult::Ok {lines, minimum_indent, offset, blank_finish } = Parser::read_indented_block(
867        src_lines,
868        line_cursor.relative_offset(),
869        false,
870        true,
871        Some(body_indent),
872        None,
873        false,
874    ) {
875        (lines, offset)
876    } else {
877        return TransitionResult::Failure {
878            message: format!("Could not read the legend contents of the figure on line {}. Computer says no...", line_cursor.sum_total()),
879            doctree: doctree
880        };
881    };
882
883    let (mut doctree, mut nested_state_stack) = match Parser::new(
884        lines,
885        doctree,
886        body_indent,
887        line_cursor.sum_total(),
888        State::ListTable,
889        section_level,
890    ).parse() {
891        ParsingResult::EOF {
892            doctree,
893            state_stack,
894        } => (doctree, state_stack),
895        ParsingResult::EmptyStateStack {
896            doctree,
897            state_stack,
898        } => (doctree, state_stack),
899        ParsingResult::Failure { message, doctree } => {
900            return TransitionResult::Failure {
901                message: format!(
902                    "Error when parsing a list-table on line {}: {}",
903                    line_cursor.sum_total(),
904                    message
905                ),
906                doctree: doctree,
907            };
908        }
909    };
910
911    // Focus back on list-table
912    while nested_state_stack.len() > 1 {
913        nested_state_stack.pop();
914        doctree = doctree.focus_on_parent()
915    }
916
917    if let TreeNodeType::ListTable { .. } = doctree.shared_data() {
918        // A-Ok
919    } else {
920        return TransitionResult::Failure {
921            message: format!("Not focused on list-table after parsing its contents starting on line {}. Computer says no...", line_cursor.sum_total()),
922            doctree: doctree
923        };
924    };
925
926    // Check largest number of columns and validate list at the same time
927
928    let n_of_columns = {
929        let mut max_cols: u32 = 0;
930
931        if let Some(children) = doctree.shared_children() {
932            if let Some(child_list) = children.get(0) {
933                if let TreeNodeType::BulletList { .. } = child_list.shared_data() {
934                    if let Some(list_items) = child_list.shared_children() {
935                        // Go over sublists and count the number of children in them
936                        for list_item in list_items {
937                            if let Some(children) = list_item.shared_children() {
938                                if let Some(nested_list) = children.get(0) {
939                                    if let TreeNodeType::BulletList { .. } = nested_list.shared_data() {
940                                        if let Some(items) = nested_list.shared_children() {
941                                            let row_entry_count = items
942                                                .iter()
943                                                .filter(
944                                                    |item| {
945                                                        if let TreeNodeType::BulletListItem { .. } = item.shared_data() {
946                                                            true
947                                                        } else {
948                                                            false
949                                                        }
950                                                    }
951                                                )
952                                                .count() as u32;
953                                            max_cols = std::cmp::max(max_cols, row_entry_count);
954                                        } else {
955                                            return TransitionResult::Failure {
956                                                message: format!("Second level list has no children inside list-table before line {}. Computer says no...", line_cursor.sum_total()),
957                                                doctree: doctree
958                                            };
959                                        }
960                                    } else {
961                                        return TransitionResult::Failure {
962                                            message: format!("No second level bullet list inside list-table before line {}. Computer says no...", line_cursor.sum_total()),
963                                            doctree: doctree
964                                        };
965                                    }
966                                } else {
967                                    return TransitionResult::Failure {
968                                        message: format!("List item in list-table on line {} does not contain children. Computer says no...", line_cursor.sum_total()),
969                                        doctree: doctree
970                                    };
971                                }
972                            } else {
973                                return TransitionResult::Failure {
974                                    message: format!("First level list item inside list-table on line {} has no children. Computer says no...", line_cursor.sum_total()),
975                                    doctree: doctree
976                                };
977                            }
978                        }
979                    } else {
980                        return TransitionResult::Failure {
981                            message: format!("Bullet list in list-table on line {} cannot have children? Computer says no...", line_cursor.sum_total()),
982                            doctree: doctree
983                        };
984                    }
985                } else {
986                    return TransitionResult::Failure {
987                        message: format!("First child if list-table on line {} is not a bullet list. Computer says no...", line_cursor.sum_total()),
988                        doctree: doctree
989                    };
990                }
991            } else {
992                return TransitionResult::Failure {
993                    message: format!(
994                        "List-table on line {} has no children. Computer says no...",
995                        line_cursor.sum_total()
996                    ),
997                    doctree: doctree,
998                };
999            }
1000        } else {
1001            return TransitionResult::Failure {
1002                message: format!(
1003                    "List-table before line {} cannot have children? Computer says no...",
1004                    line_cursor.sum_total()
1005                ),
1006                doctree: doctree,
1007            };
1008        }
1009        max_cols
1010    };
1011
1012    // Set column widths, if not yet set
1013
1014    if let TreeNodeType::ListTable { widths, .. } = doctree.mut_node_data() {
1015        if widths.is_none() {
1016            *widths = Some(
1017                TableColWidths::Columns(
1018                    std::iter::repeat(1.0 / n_of_columns as f64)
1019                        .take(n_of_columns as usize)
1020                        .collect(),
1021                )
1022            )
1023        }
1024    } else {
1025        return TransitionResult::Failure {
1026            message: format!("Not focused on list-table before line {}, after validating said table. Computer says no...", line_cursor.sum_total()),
1027            doctree: doctree
1028        };
1029    }
1030
1031    TransitionResult::Success {
1032        doctree: doctree,
1033        push_or_pop: PushOrPop::Push(vec![State::ListTable]),
1034        line_advance: LineAdvance::Some(offset),
1035    }
1036}
1037
1038pub fn parse_contents() {
1039    todo!()
1040}
1041
1042pub fn parse_section_numbering() {
1043    todo!()
1044}
1045
1046pub fn parse_header_or_footer() {
1047    todo!()
1048}
1049
1050pub fn parse_target_notes() {
1051    todo!()
1052}
1053
1054pub fn parse_footnotes() {
1055    todo!()
1056}
1057
1058pub fn parse_citations() {
1059    todo!()
1060}
1061
1062pub fn parse_meta() {
1063    todo!()
1064}
1065
1066/// Parses in "include" directive for its argument and options.
1067/// Generates an "include" node in the parse tree with the given options.
1068pub fn parse_include(
1069    src_lines: &Vec<String>,
1070    mut doctree: DocTree,
1071    line_cursor: &mut LineCursor,
1072    first_indent: usize,
1073    body_indent: usize,
1074    empty_after_marker: bool,
1075    section_level: usize,
1076) -> TransitionResult {
1077
1078    let uri = match scan_directive_arguments(src_lines, line_cursor, body_indent, Some(first_indent), empty_after_marker) {
1079        Some(arg) => arg.join(""),
1080        None => return TransitionResult::Failure {
1081            message: format!("Include directive on line {} did not have a file URI as an argument.", line_cursor.sum_total()),
1082            doctree: doctree
1083        }
1084    };
1085
1086    let (
1087        start_line,
1088        end_line,
1089        start_after,
1090        end_before,
1091        literal,
1092        code,
1093        number_lines,
1094        encoding,
1095        tab_width,
1096        name,
1097        class
1098    ) = if let Some(mut options) = scan_directive_options
1099    (src_lines, line_cursor, body_indent) {
1100        let start_line = if let Some(option) = options.remove("start-line") {
1101            match option.parse::<u32>() {
1102                Ok(num) => Some(num),
1103                Err(_) => None
1104            }
1105        } else {
1106            None
1107        };
1108        let end_line = if let Some(option) = options.remove("end-line") {
1109            match option.parse::<u32>() {
1110                Ok(num) => Some(num),
1111                Err(_) => None
1112            }
1113        } else {
1114            None
1115        };
1116        let start_after = if let Some(option) = options.remove("start-after") {
1117            match option.parse::<u32>() {
1118                Ok(num) => Some(num),
1119                Err(_) => None
1120            }
1121        } else {
1122            None
1123        };
1124        let end_before = if let Some(option) = options.remove("end-before") {
1125            match option.parse::<u32>() {
1126                Ok(num) => Some(num),
1127                Err(_) => None
1128            }
1129        } else {
1130            None
1131        };
1132        let literal = if let Some(option) = options.remove("literal") {
1133            true
1134        } else {
1135            false
1136        };
1137        let code = if let Some(language) = options.remove("code") {
1138            if language.trim().is_empty() {
1139                Some(None)
1140            } else {
1141                Some(Some(language))
1142            }
1143        } else {
1144            None
1145        };
1146        let number_lines = if let Some(option) = options.remove("code") {
1147            if option.trim().is_empty() {
1148                Some(None)
1149            } else {
1150                match option.parse::<u32>() {
1151                    Ok(number) => Some(Some(number)),
1152                    Err(_) => Some(None)
1153                }
1154            }
1155        } else {
1156            None
1157        };
1158        let encoding = if let Some(encoding) = options.remove("encoding") {
1159            Some(encoding)
1160        } else {
1161            None
1162        };
1163        let tab_width = if let Some(option) = options.remove("tab-width") {
1164            match option.parse::<u32>() {
1165                Ok(number) => Some(number),
1166                Err(_) => None
1167            }
1168        } else {
1169            None
1170        };
1171        let name = if let Some(option) = options.remove("name") {
1172            Some(option)
1173        } else {
1174            None
1175        };
1176        let class = if let Some(option) = options.remove("class") {
1177            Some(option)
1178        } else {
1179            None
1180        };
1181
1182        (start_line, end_line, start_after, end_before, literal, code, number_lines, encoding, tab_width, name, class)
1183
1184    } else {
1185        (None, None, None, None, false, None, None, None, None, None, None)
1186    };
1187
1188    let include_node_data = TreeNodeType::Include {
1189        uri: uri,
1190        start_line: start_line,
1191        end_line: end_line,
1192        start_after: start_after,
1193        end_before: end_before,
1194        literal: literal,
1195        code: code,
1196        number_lines: number_lines,
1197        encoding: encoding,
1198        tab_width: tab_width,
1199        name: name,
1200        class: class
1201    };
1202
1203    doctree = match doctree.push_data(include_node_data) {
1204        Ok(tree) => tree,
1205        Err(tree) => return TransitionResult::Failure {
1206            message: format!("Node insertion error on line {}.", line_cursor.sum_total()),
1207            doctree: tree
1208        }
1209    };
1210
1211    TransitionResult::Success {
1212        doctree: doctree,
1213        push_or_pop: PushOrPop::Neither,
1214        line_advance: LineAdvance::None
1215    }
1216}
1217
1218pub fn parse_raw() {
1219    todo!()
1220}
1221
1222pub fn parse_class(
1223    src_lines: &Vec<String>,
1224    mut doctree: DocTree,
1225    line_cursor: &mut LineCursor,
1226    first_indent: usize,
1227    body_indent: usize,
1228    empty_after_marker: bool,
1229    section_level: usize,
1230) -> TransitionResult {
1231    let classes = if let Some(classes) = scan_directive_arguments(
1232        src_lines,
1233        line_cursor,
1234        body_indent,
1235        Some(first_indent),
1236        empty_after_marker,
1237    ) {
1238        classes
1239            .iter()
1240            .filter(|s| !s.is_empty())
1241            .map(|s| s.to_string())
1242            .collect::<Vec<String>>()
1243    } else {
1244        return TransitionResult::Failure {
1245            message: format!(
1246                "Class directive on line {} doesn't provide any classes. Computer says no...",
1247                line_cursor.sum_total()
1248            ),
1249            doctree: doctree,
1250        };
1251    };
1252
1253    let class_node = TreeNodeType::Class {
1254        body_indent: body_indent,
1255        classes: classes,
1256    };
1257
1258    doctree = match doctree.push_data_and_focus(class_node) {
1259        Ok(tree) => tree,
1260        Err(tree) => {
1261            return TransitionResult::Failure {
1262                message: format!(
1263                    "Failed to push class node to tree on line {}...",
1264                    line_cursor.sum_total()
1265                ),
1266                doctree: tree,
1267            }
1268        }
1269    };
1270
1271    let (lines, offset) = if let IndentedBlockResult::Ok {lines, minimum_indent, offset, blank_finish } = Parser::read_indented_block(
1272        src_lines,
1273        line_cursor.relative_offset(),
1274        false,
1275        true,
1276        Some(body_indent),
1277        None,
1278        false,
1279    ) {
1280        (lines, offset)
1281    } else {
1282        return TransitionResult::Failure {
1283            message: format!(
1284                "Could not parse class contents starting from line {}. Computer says no...",
1285                line_cursor.sum_total()
1286            ),
1287            doctree: doctree,
1288        };
1289    };
1290
1291    let (doctree, nested_state_stack) = match Parser::new(
1292        lines,
1293        doctree,
1294        body_indent,
1295        line_cursor.sum_total(),
1296        State::Body,
1297        section_level,
1298    ).parse() {
1299        ParsingResult::EOF {
1300            doctree,
1301            state_stack,
1302        } => (doctree, state_stack),
1303        ParsingResult::EmptyStateStack {
1304            doctree,
1305            state_stack,
1306        } => (doctree, state_stack),
1307        ParsingResult::Failure { message, doctree } => {
1308            return TransitionResult::Failure {
1309                message: format!(
1310                    "Error when parsing a class on line {}: {}",
1311                    line_cursor.sum_total(),
1312                    message
1313                ),
1314                doctree: doctree,
1315            }
1316        }
1317    };
1318
1319    TransitionResult::Success {
1320        doctree: doctree,
1321        push_or_pop: PushOrPop::Push(nested_state_stack),
1322        line_advance: LineAdvance::None,
1323    }
1324}
1325
1326pub fn parse_role() {
1327    todo!()
1328}
1329
1330pub fn parse_default_role() {
1331    todo!()
1332}
1333
1334pub fn parse_title() {
1335    todo!()
1336}
1337
1338pub fn restucturetext_test_directive() {
1339    todo!()
1340}
1341
1342// ========================
1343//  Sphinx-specific directives
1344// ========================
1345
1346pub fn parse_sphinx_toctree() {
1347    todo!()
1348}
1349
1350pub fn parse_sphinx_versionadded() {
1351    todo!()
1352}
1353
1354pub fn parse_sphinx_versionchanged() {
1355    todo!()
1356}
1357
1358pub fn parse_sphinx_deprecated() {
1359    todo!()
1360}
1361
1362pub fn parse_sphinx_seealso() {
1363    todo!()
1364}
1365
1366pub fn parse_sphinx_centered() {
1367    todo!()
1368}
1369
1370pub fn parse_sphinx_hlist() {
1371    todo!()
1372}
1373
1374pub fn parse_sphinx_highlight() {
1375    todo!()
1376}
1377
1378/// A parser for the Sphinx-specific `code-block` directive. See https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block
1379/// for explanations of different settings and arguments.
1380pub fn parse_sphinx_code_block(
1381    src_lines: &Vec<String>,
1382    mut doctree: DocTree,
1383    line_cursor: &mut LineCursor,
1384    base_indent: usize,
1385    empty_after_marker: bool,
1386    body_indent: usize,
1387    first_indent: Option<usize>,
1388) -> TransitionResult {
1389    // Read directive argument: the formal language (should be recognized by Pygments)
1390    let formal_language = if let Some(arg) = scan_directive_arguments(
1391        src_lines,
1392        line_cursor,
1393        body_indent,
1394        first_indent,
1395        empty_after_marker,
1396    ) {
1397        arg.join("")
1398    } else {
1399        String::from("python") // the Sphinx "highlight_language" setting default
1400    };
1401
1402    // Read the settings...
1403    let (linenos, lineno_start, emphasize_lines, caption, name, dedent, force) =
1404        if let Some(mut settings) = scan_directive_options(
1405            src_lines, line_cursor, body_indent
1406        ){
1407            let mut linenos = if let Some(linenos) = settings.remove("linenos") {
1408                true
1409            } else {
1410                false
1411            };
1412            let lineno_start = if let Some(start_line) = settings.remove("lineno-start") {
1413                if let Ok(number) = start_line.parse::<usize>() {
1414                    linenos = true;
1415                    Some(number)
1416                } else {
1417                    None
1418                }
1419            } else {
1420                None
1421            };
1422            let emphasize_lines = if let Some(line_numbers) = settings.remove("emphasize-lines")
1423            {
1424                let emph_lines = line_numbers
1425                    .split(",")
1426                    .filter(|s| !s.trim().is_empty())
1427                    .map(|s| s.trim())
1428                    .filter_map(|s| s.parse::<usize>().ok())
1429                    .collect::<Vec<usize>>();
1430
1431                Some(emph_lines)
1432            } else {
1433                None
1434            };
1435            let caption = settings.remove("caption");
1436            let name = if let Some(refname) = settings.remove("name") {
1437                Some(crate::common::normalize_refname(&refname))
1438            } else {
1439                None
1440            };
1441            let dedent = if let Some(dedent) = settings.remove("dedent") {
1442                if let Ok(dedent) = dedent.parse::<usize>() {
1443                    Some(dedent)
1444                } else {
1445                    None
1446                }
1447            } else {
1448                None
1449            };
1450            let force = if let Some(force) = settings.remove("force") {
1451                true
1452            } else {
1453                false
1454            };
1455
1456            (
1457                linenos,
1458                lineno_start,
1459                emphasize_lines,
1460                caption,
1461                name,
1462                dedent,
1463                force,
1464            )
1465        } else {
1466            (false, None, None, None, None, None, false)
1467        };
1468
1469    // Construct node from settings and read content...
1470
1471    let (code_text, offset) = match Parser::read_indented_block(
1472        src_lines,
1473        line_cursor.relative_offset(),
1474        false,
1475        true,
1476        Some(body_indent),
1477        None,
1478        false,
1479    ) {
1480        IndentedBlockResult::Ok {mut lines, minimum_indent, offset, blank_finish } => {
1481            // Remove empty lines from front
1482            lines = lines
1483                .iter()
1484                .skip_while(|line| line.is_empty())
1485                .map(|s| s.to_string())
1486                .collect();
1487
1488            // Remove empty lines from back
1489            while let Some(line) = lines.last_mut() {
1490                if line.is_empty() {
1491                    lines.pop();
1492                } else {
1493                    break;
1494                }
1495            }
1496
1497            (lines.join("\n") + "\n", offset)
1498        }
1499        _ => {
1500            return TransitionResult::Failure {
1501                message: format!(
1502                    "Error when parsing a Sphinx code block on line {}.",
1503                    line_cursor.sum_total(),
1504                ),
1505                doctree: doctree,
1506            }
1507        }
1508    };
1509
1510    let code_block_data = TreeNodeType::SphinxCodeBlock {
1511        language: formal_language,
1512        linenos: linenos,
1513        lineno_start: lineno_start,
1514        emphasize_lines: emphasize_lines,
1515        caption: caption,
1516        name: name,
1517        dedent: dedent,
1518        force: force,
1519        code_text: code_text,
1520    };
1521
1522    doctree = match doctree.push_data(code_block_data) {
1523        Ok(tree) => tree,
1524        Err(tree) => {
1525            return TransitionResult::Failure {
1526                message: format!(
1527                    "Erro when parsing Sphinx code block on line {}. Computer says no...",
1528                    line_cursor.sum_total()
1529                ),
1530                doctree: tree,
1531            }
1532        }
1533    };
1534
1535    TransitionResult::Success {
1536        doctree: doctree,
1537        push_or_pop: PushOrPop::Neither,
1538        line_advance: LineAdvance::Some(offset),
1539    }
1540}
1541
1542pub fn parse_sphinx_literalinclude() {
1543    todo!()
1544}
1545
1546pub fn parse_sphinx_glossary() {
1547    todo!()
1548}
1549
1550pub fn parse_sphinx_sectionauthor() {
1551    todo!()
1552}
1553
1554pub fn parse_sphinx_codeauthor() {
1555    todo!()
1556}
1557
1558pub fn parse_sphinx_index() {
1559    todo!()
1560}
1561
1562pub fn parse_sphinx_only(
1563    src_lines: &Vec<String>,
1564    mut doctree: DocTree,
1565    line_cursor: &mut LineCursor,
1566    empty_after_marker: bool,
1567    first_indent: usize,
1568    body_indent: usize,
1569) -> TransitionResult {
1570
1571
1572    /// Directive `only` tags that are always known to the Sphinx parser.
1573    /// These work like expressions in predicate logic and can be combined with
1574    /// ` and `, ` or ` and grouped with parentheses.
1575
1576    /// They should be included with the directive argument.
1577    const ALWAYS_DEFINED_TAGS: &[&str] = &["html", "latex", "text"];
1578
1579    let expression = match scan_directive_arguments(src_lines, line_cursor, body_indent, Some(first_indent), empty_after_marker) {
1580        Some(lines) => lines.join(" "),
1581        None => String::new()
1582    };
1583
1584    if expression.is_empty() {
1585        return TransitionResult::Failure {
1586            message: format!(
1587                r#"The expression of an "only" Sphinx directive on line {} should not be empty. Computer says no..."#,
1588                line_cursor.sum_total()
1589            ),
1590            doctree: doctree,
1591        };
1592    }
1593
1594    let only_node = TreeNodeType::SphinxOnly {
1595        expression: expression,
1596        body_indent: body_indent,
1597    };
1598
1599    doctree = match doctree.push_data_and_focus(only_node) {
1600        Ok(tree) => tree,
1601        Err(tree) => {
1602            return TransitionResult::Failure {
1603                message: format!(
1604                    "Node insertion error on line {}. Computer says no...",
1605                    line_cursor.sum_total()
1606                ),
1607                doctree: tree,
1608            }
1609        }
1610    };
1611
1612    TransitionResult::Success {
1613        doctree: doctree,
1614        push_or_pop: PushOrPop::Push(vec![State::Body]),
1615        line_advance: LineAdvance::Some(1),
1616    }
1617}
1618
1619pub fn parse_sphinx_tabularcolumns() {
1620    todo!()
1621}
1622
1623pub fn parse_sphinx_math_block() {
1624    todo!()
1625}
1626
1627pub fn parse_sphinx_productionlist() {
1628    todo!()
1629}
1630
1631// ========================
1632//  A+-specific directives
1633// ========================
1634
1635pub fn parse_aplus_questionnaire(
1636    src_lines: &Vec<String>,
1637    mut doctree: DocTree,
1638    line_cursor: &mut LineCursor,
1639    base_indent: usize,
1640    empty_after_marker: bool,
1641    first_indent: usize,
1642    body_indent: usize,
1643) -> TransitionResult {
1644    let (key, difficulty, max_points): (String, String, String) = match scan_directive_arguments(
1645            src_lines,
1646            line_cursor,
1647            body_indent,
1648            Some(first_indent),
1649            empty_after_marker,
1650    ) {
1651        Some(lines) => aplus_key_difficulty_and_max_points(lines.join(" ").as_str(), line_cursor),
1652        None => return TransitionResult::Failure {
1653            message: format!(
1654                "A+ questionnaire on line {} was not given arguments. Computer says no...",
1655                line_cursor.sum_total()
1656            ),
1657            doctree: doctree,
1658        }
1659    };
1660
1661    let (
1662        submissions,
1663        points_to_pass,
1664        feedback,
1665        title,
1666        no_override,
1667        pick_randomly,
1668        preserve_questions_between_attempts,
1669        category,
1670        status,
1671        reveal_model_at_max_submissions,
1672        show_model,
1673        allow_assistant_viewing,
1674        allow_assistant_grading,
1675    ) = if let Some(mut options) = scan_directive_options(src_lines, line_cursor, body_indent) {
1676        (
1677            options.remove("submissions"),
1678            options.remove("points-to-pass"),
1679            options.remove("feedback"),
1680            options.remove("title"),
1681            options.remove("no_override"),
1682            options.remove("pick_randomly"),
1683            options.remove("preserve-questions-between-attempts"),
1684            options.remove("category"),
1685            options.remove("status"),
1686            options.remove("reveal-model-at-max-submissions"),
1687            options.remove("show-model"),
1688            options.remove("allow-assistant-viewing"),
1689            options.remove("allow-assistant-grading"),
1690        )
1691    } else {
1692        (
1693            None, None, None, None, None, None, None, None, None, None, None, None, None,
1694        )
1695    };
1696
1697    use crate::common::QuizPoints;
1698
1699    let questionnaire_node = TreeNodeType::AplusQuestionnaire {
1700        body_indent: body_indent,
1701        key: key,
1702        difficulty: if difficulty.is_empty() {
1703            None
1704        } else {
1705            Some(difficulty)
1706        },
1707        max_points: if let Ok(result) = max_points.parse::<QuizPoints>() {
1708            Some(result)
1709        } else {
1710            None
1711        },
1712        points_from_children: 0,
1713        submissions: submissions,
1714        points_to_pass: points_to_pass,
1715        feedback: feedback,
1716        title: title,
1717        no_override: no_override,
1718        pick_randomly: pick_randomly,
1719        preserve_questions_between_attempts: preserve_questions_between_attempts,
1720        category: category,
1721        status: status,
1722        reveal_model_at_max_submissions: reveal_model_at_max_submissions,
1723        show_model: show_model,
1724        allow_assistant_viewing: allow_assistant_viewing,
1725        allow_assistant_grading: allow_assistant_grading,
1726    };
1727
1728    doctree = match doctree.push_data_and_focus(questionnaire_node) {
1729        Ok(tree) => tree,
1730        Err(tree) => {
1731            return TransitionResult::Failure {
1732                message: format!(
1733                    "Node insertion error on line {}. Computer says no...",
1734                    line_cursor.sum_total()
1735                ),
1736                doctree: tree,
1737            }
1738        }
1739    };
1740
1741    TransitionResult::Success {
1742        doctree: doctree,
1743        push_or_pop: PushOrPop::Push(vec![State::AplusQuestionnaire]),
1744        line_advance: LineAdvance::None,
1745    }
1746}
1747
1748/// A `pick-one` type questionnaire question parser.
1749pub fn parse_aplus_pick_one(
1750    src_lines: &Vec<String>,
1751    mut doctree: DocTree,
1752    line_cursor: &mut LineCursor,
1753    first_indent: usize,
1754    body_indent: usize,
1755    empty_after_marker: bool,
1756) -> TransitionResult {
1757
1758
1759    /// Correct answers in `pick-one` and `pick-any` directives are marked with `*`.
1760    /// A `pick-any` question may have neutral options, which are marked with `?`.
1761    /// Neutral options are always counted as correct, whether the student selected them or not.
1762    /// Initially selected options may be set with `+`.
1763    /// The initially selected options are pre-selected when the exercise is loaded.
1764    /// The `+` character is written before `*` or `?` if they are combined.
1765    const APLUS_PICK_ONE_CHOICE_PATTERN: &'static str =
1766        r"^(\s*)(?P<pre_selected>\+)?(?P<correct>\*)?(?P<label>\S+)\.[ ]+(?P<answer>.+)";
1767    const APLUS_PICK_HINT_PATTERN: &'static str =
1768        r"^(\s*)(?P<show_not_answered>!)?(?P<label>\S+)[ ]*§[ ]*(?P<hint>.+)";
1769
1770    use regex::{Captures, Regex};
1771
1772    lazy_static::lazy_static! {
1773        static ref CHOICE_RE: Regex = Regex::new(APLUS_PICK_ONE_CHOICE_PATTERN).unwrap();
1774        static ref HINT_RE: Regex = Regex::new(APLUS_PICK_HINT_PATTERN).unwrap();
1775    }
1776
1777    // Parsing the directive arguments
1778
1779    use crate::common::QuizPoints;
1780
1781    let points: QuizPoints = match scan_directive_arguments(
1782        src_lines,
1783        line_cursor,
1784        body_indent,
1785        Some(first_indent),
1786        empty_after_marker,
1787    ) {
1788        Some(lines) => match lines.join(" ").as_str().parse() {
1789            Ok(points) => points,
1790            Err(e) => return TransitionResult::Failure {
1791                message: format!("Quiz question points preceding line {} could not be parsed into an integer. Computer says no...", line_cursor.sum_total()),
1792                doctree: doctree
1793            }
1794        }
1795        None => return TransitionResult::Failure {
1796            message: format!(
1797                "No points provided for pick-one question on line {}. Computer says no...",
1798                line_cursor.sum_total()
1799            ),
1800            doctree: doctree,
1801        }
1802    };
1803
1804    if let TreeNodeType::AplusQuestionnaire {
1805        points_from_children,
1806        ..
1807    } = doctree.mut_node_data() {
1808        *points_from_children += points;
1809    }
1810
1811    // Parsing directive options
1812
1813    let (class, required, key, dropdown) = if let Some(mut options) =
1814        scan_directive_options
1815        (src_lines, line_cursor, body_indent)
1816    {
1817        (
1818            options.remove("class"),
1819            options.remove("required"),
1820            options.remove("key"),
1821            options.remove("dropdown"),
1822        )
1823    } else {
1824        (None, None, None, None)
1825    };
1826
1827    Parser::skip_empty_lines(src_lines, line_cursor);
1828
1829    // Generating and focusing on node
1830
1831    let pick_one_node = TreeNodeType::AplusPickOne {
1832        body_indent: body_indent,
1833        class: class,
1834        points: points,
1835        required: if required.is_some() { true } else { false },
1836        key: key,
1837        dropdown: if dropdown.is_some() { true } else { false },
1838    };
1839
1840    doctree = match doctree.push_data_and_focus(pick_one_node) {
1841        Ok(tree) => tree,
1842        Err(tree) => {
1843            return TransitionResult::Failure {
1844                message: format!(
1845                    "Node insertion error on line {}. Computer says no...",
1846                    line_cursor.sum_total()
1847                ),
1848                doctree: tree,
1849            }
1850        }
1851    };
1852
1853    // Check for assignment
1854
1855    Parser::skip_empty_lines(src_lines, line_cursor);
1856
1857    let start_line = match src_lines.get(line_cursor.relative_offset()) {
1858        Some(line) => line,
1859        None => return TransitionResult::Failure {
1860            message: format!(
1861                "Input overflow on line {} when parsing pick-one assignment. Computer says no...",
1862                line_cursor.sum_total()
1863            ),
1864            doctree: doctree
1865        }
1866    };
1867
1868    let assignment_inline_nodes: Vec<TreeNodeType> = if !CHOICE_RE.is_match(start_line) {
1869        let (block_lines, offset) = match Parser::read_text_block(src_lines, line_cursor.relative_offset(),  true, true, Some(body_indent),true) {
1870            TextBlockResult::Ok {lines, offset } => (lines, offset),
1871            TextBlockResult::Err { lines, offset } => return TransitionResult::Failure {
1872                message: format!("Could not read pick-one assignment lines starting on line {}. Computer says no...", line_cursor.sum_total()),
1873                doctree: doctree
1874            }
1875        };
1876        let inline_nodes = match Parser::inline_parse(block_lines.join("\n"), None, line_cursor) {
1877            InlineParsingResult::Nodes(nodes) => nodes,
1878            _ => return TransitionResult::Failure {
1879                message: format!("Could not parse pick-one assignment for inline nodes on line {}. Computer says no...", line_cursor.sum_total()),
1880                doctree: doctree
1881            }
1882        };
1883
1884        line_cursor.increment_by(1);
1885
1886        inline_nodes
1887    } else {
1888        Vec::new()
1889    };
1890
1891    // Add assignment node (paragraph) to tree
1892
1893    Parser::skip_empty_lines(src_lines, line_cursor);
1894
1895    if !assignment_inline_nodes.is_empty() {
1896        let assignment_node = TreeNodeType::Paragraph {
1897            indent: body_indent,
1898        };
1899        doctree = match doctree.push_data_and_focus(assignment_node) {
1900            Ok(tree) => tree,
1901            Err(tree) => {
1902                return TransitionResult::Failure {
1903                    message: format!(
1904                        "Node insertion error on line {}. Computer says no...",
1905                        line_cursor.sum_total()
1906                    ),
1907                    doctree: tree,
1908                }
1909            }
1910        };
1911        for node in assignment_inline_nodes {
1912            doctree = match doctree.push_data(node) {
1913                Ok(tree) => tree,
1914                Err(tree) => {
1915                    return TransitionResult::Failure {
1916                        message: format!(
1917                            "Node insertion error on line {}. Computer says no...",
1918                            line_cursor.sum_total()
1919                        ),
1920                        doctree: tree,
1921                    }
1922                }
1923            };
1924        }
1925        doctree = doctree.focus_on_parent()
1926    }
1927
1928    Parser::skip_empty_lines(src_lines, line_cursor);
1929
1930    // Read question choices
1931
1932    doctree = match doctree.push_data_and_focus(TreeNodeType::AplusPickChoices {
1933        body_indent: body_indent,
1934    }) {
1935        Ok(tree) => tree,
1936        Err(tree) => {
1937            return TransitionResult::Failure {
1938                message: format!(
1939                    "Node insertion error on line {}. Computer says no...",
1940                    line_cursor.sum_total()
1941                ),
1942                doctree: tree,
1943            }
1944        }
1945    };
1946
1947    while let Some(current_line) = src_lines.get(line_cursor.relative_offset()) {
1948        let indent = current_line
1949            .chars()
1950            .take_while(|c| c.is_whitespace())
1951            .count();
1952
1953        if indent != body_indent {
1954            break;
1955        }
1956
1957        let captures: Captures = if let Some(capts) = CHOICE_RE.captures(current_line) {
1958            capts
1959        } else {
1960            break;
1961        };
1962
1963        let label = captures.name("label").unwrap().as_str().to_string();
1964        let pre_selected = captures.name("pre_selected");
1965        let correct = captures.name("correct");
1966        let answer = if let Some(capture) = captures.name("answer") {
1967            capture.as_str()
1968        } else {
1969            ""
1970        };
1971
1972        if answer.trim().is_empty() {
1973            return TransitionResult::Failure {
1974                message: format!("Discovered a pick-one answer without content on line {}. Computer says no...", line_cursor.sum_total()),
1975                doctree: doctree
1976            };
1977        }
1978
1979        let answer_nodes: Vec<TreeNodeType> = match Parser::inline_parse(answer.to_string(), None, line_cursor) {
1980            InlineParsingResult::Nodes(nodes) => nodes,
1981            _ => return TransitionResult::Failure {
1982                message: format!("Could not parse pick-one answer on line {} for inline nodes. Computer says no...", line_cursor.sum_total()),
1983                doctree: doctree
1984            }
1985        };
1986
1987        let choice_node = TreeNodeType::AplusPickChoice {
1988            label: label,
1989            is_pre_selected: pre_selected.is_some(),
1990            is_correct: correct.is_some(),
1991            is_neutral: false, // pick-one nodes don't have this set
1992        };
1993
1994        doctree = match doctree.push_data_and_focus(choice_node) {
1995            Ok(tree) => tree,
1996            Err(tree) => {
1997                return TransitionResult::Failure {
1998                    message: format!(
1999                        "Node insertion error on line {}. Computer says no...",
2000                        line_cursor.sum_total()
2001                    ),
2002                    doctree: tree,
2003                }
2004            }
2005        };
2006        for node in answer_nodes {
2007            doctree = match doctree.push_data(node) {
2008                Ok(tree) => tree,
2009                Err(tree) => {
2010                    return TransitionResult::Failure {
2011                        message: format!(
2012                            "Node insertion error on line {}. Computer says no...",
2013                            line_cursor.sum_total()
2014                        ),
2015                        doctree: tree,
2016                    }
2017                }
2018            };
2019        }
2020        doctree = doctree.focus_on_parent();
2021
2022        line_cursor.increment_by(1);
2023    }
2024
2025    if doctree.n_of_children() == 0 {
2026        return TransitionResult::Failure {
2027            message: format!(
2028                "Found no choices for pick-one question on line {}. Computer says no...",
2029                line_cursor.sum_total()
2030            ),
2031            doctree: doctree,
2032        };
2033    }
2034
2035    doctree = doctree.focus_on_parent();
2036
2037    // Read possible hints inside the answers environment
2038
2039    Parser::skip_empty_lines(src_lines, line_cursor);
2040
2041    doctree = match doctree.push_data_and_focus(TreeNodeType::AplusQuestionnaireHints {
2042        body_indent: body_indent,
2043    }) {
2044        Ok(tree) => tree,
2045        Err(tree) => {
2046            return TransitionResult::Failure {
2047                message: format!(
2048                    "Node insertion error on line {}. Computer says no...",
2049                    line_cursor.sum_total()
2050                ),
2051                doctree: tree,
2052            }
2053        }
2054    };
2055
2056    while let Some(current_line) = src_lines.get(line_cursor.relative_offset()) {
2057        let indent = if !current_line.is_empty() {
2058            current_line
2059                .chars()
2060                .take_while(|c| c.is_whitespace())
2061                .count()
2062        } else {
2063            body_indent
2064        };
2065
2066        if indent != body_indent {
2067            break;
2068        }
2069
2070        let captures = if let Some(capts) = HINT_RE.captures(current_line) {
2071            capts
2072        } else {
2073            break;
2074        };
2075
2076        let show_not_answered = captures.name("show_not_answered");
2077        let label = match captures.name("label") {
2078            Some(label) => label.as_str().to_string(),
2079            None => {
2080                return TransitionResult::Failure {
2081                    message: format!(
2082                        "No enumerator for pick-one hint on line {}. Computer says no...",
2083                        line_cursor.sum_total()
2084                    ),
2085                    doctree: doctree,
2086                }
2087            }
2088        };
2089        let hint: &str = if let Some(hint) = captures.name("hint") {
2090            hint.as_str().trim()
2091        } else {
2092            return TransitionResult::Failure {
2093                message: format!(
2094                    "No hint text for pick-one hint on line {}. Computer says no...",
2095                    line_cursor.sum_total()
2096                ),
2097                doctree: doctree,
2098            };
2099        };
2100
2101        if hint.is_empty() {
2102            return TransitionResult::Failure {
2103                message: format!(
2104                    "Empty  hint text for hint on line {}. Computer says no...",
2105                    line_cursor.sum_total()
2106                ),
2107                doctree: doctree,
2108            };
2109        }
2110
2111        let hint_nodes: Vec<TreeNodeType> = match Parser::inline_parse(hint.to_string(), None, line_cursor) {
2112            InlineParsingResult::Nodes(nodes) => nodes,
2113            _ => return TransitionResult::Failure {
2114                message: format!("Could not parse pick-one answer on line {} for inline nodes. Computer says no...", line_cursor.sum_total()),
2115                doctree: doctree
2116            }
2117        };
2118
2119        if hint_nodes.is_empty() {
2120            return TransitionResult::Failure {
2121                message: format!(
2122                    "No inline nodes found for pick-one hint on line {}. Computer says no...",
2123                    line_cursor.sum_total()
2124                ),
2125                doctree: doctree,
2126            };
2127        }
2128
2129        let hint_node = TreeNodeType::AplusQuestionnaireHint {
2130            label: label,
2131            show_when_not_selected: show_not_answered.is_some(),
2132            question_type: crate::common::AplusQuestionnaireType::PickOne,
2133        };
2134
2135        doctree = match doctree.push_data_and_focus(hint_node) {
2136            Ok(tree) => tree,
2137            Err(tree) => {
2138                return TransitionResult::Failure {
2139                    message: format!(
2140                        "Node insertion error on line {}. Computer says no...",
2141                        line_cursor.sum_total()
2142                    ),
2143                    doctree: tree,
2144                }
2145            }
2146        };
2147        for node in hint_nodes {
2148            doctree = match doctree.push_data(node) {
2149                Ok(tree) => tree,
2150                Err(tree) => {
2151                    return TransitionResult::Failure {
2152                        message: format!(
2153                            "Node insertion error on line {}. Computer says no...",
2154                            line_cursor.sum_total()
2155                        ),
2156                        doctree: tree,
2157                    }
2158                }
2159            };
2160        }
2161        doctree = doctree.focus_on_parent();
2162
2163        line_cursor.increment_by(1);
2164    }
2165
2166    Parser::skip_empty_lines(src_lines, line_cursor);
2167
2168    doctree = doctree.focus_on_parent(); // Focus on pick-one
2169    doctree = doctree.focus_on_parent(); // Focus on questionnaire
2170
2171    // Return with modified doctree
2172
2173    TransitionResult::Success {
2174        doctree: doctree,
2175        push_or_pop: PushOrPop::Neither,
2176        line_advance: LineAdvance::None,
2177    }
2178}
2179
2180/// A `pick-any` type questionnaire question parser.
2181pub fn parse_aplus_pick_any(
2182    src_lines: &Vec<String>,
2183    mut doctree: DocTree,
2184    line_cursor: &mut LineCursor,
2185    first_indent: usize,
2186    body_indent: usize,
2187    empty_after_marker: bool,
2188) -> TransitionResult {
2189
2190    const APLUS_PICK_ANY_CHOICE_PATTERN: &'static str = r"^(\s*)(?P<pre_selected>\+)?(?:(?P<neutral>\?)|(?P<correct>\*))?(?P<label>\S+)\.[ ]+(?P<answer>.+)";
2191    const APLUS_PICK_HINT_PATTERN: &'static str =
2192        r"^(\s*)(?P<show_not_answered>!)?(?P<label>\S+)[ ]*§[ ]*(?P<hint>.+)";
2193
2194    lazy_static::lazy_static! {
2195        static ref CHOICE_RE: regex::Regex = regex::Regex::new(APLUS_PICK_ANY_CHOICE_PATTERN).unwrap();
2196        static ref HINT_RE: regex::Regex = regex::Regex::new(APLUS_PICK_HINT_PATTERN).unwrap();
2197    }
2198
2199    let points: crate::common::QuizPoints = match scan_directive_arguments(
2200        src_lines,
2201        line_cursor,
2202        body_indent,
2203        Some(first_indent),
2204        empty_after_marker,
2205    ) {
2206        Some(lines) => match lines.join(" ").as_str().parse() {
2207            Ok(points) => points,
2208            Err(e) => return TransitionResult::Failure {
2209                message: format!(
2210                    "Quiz question points preceding line {} could not be parsed into an integer. Computer says no...",
2211                    line_cursor.sum_total()
2212                ),
2213                doctree: doctree
2214            }
2215        },
2216        None => return TransitionResult::Failure {
2217            message: format!(
2218                "No points provided for pick-any question on line {}. Computer says no...",
2219                line_cursor.sum_total()
2220            ),
2221            doctree: doctree,
2222        }
2223    };
2224
2225    if let TreeNodeType::AplusQuestionnaire {
2226        points_from_children,
2227        ..
2228    } = doctree.mut_node_data() {
2229        *points_from_children += points;
2230    }
2231
2232    let (
2233        class,
2234        required,
2235        key,
2236        partial_points,
2237        randomized,
2238        correct_count,
2239        preserve_questions_between_attempts,
2240    ) = if let Some(mut options) = scan_directive_options
2241    (src_lines, line_cursor, body_indent) {
2242        (
2243            options.remove("class"),
2244            options.remove("required"),
2245            options.remove("key"),
2246            options.remove("partial-points"),
2247            options.remove("randomized"),
2248            options.remove("correct-count"),
2249            options.remove("preserve-questions-between-attempts"),
2250        )
2251    } else {
2252        (None, None, None, None, None, None, None)
2253    };
2254
2255    let pick_any_node = TreeNodeType::AplusPickAny {
2256        body_indent: body_indent,
2257        points: points,
2258        class: class,
2259        required: if required.is_some() { true } else { false },
2260        key: key,
2261        partial_points: if partial_points.is_some() {
2262            true
2263        } else {
2264            false
2265        },
2266        randomized: if randomized.is_some() && correct_count.is_some() {
2267            true
2268        } else {
2269            false
2270        },
2271        correct_count: if randomized.is_some() && correct_count.is_some() {
2272            if let Ok(result) = correct_count.unwrap().parse() {
2273                Some(result)
2274            } else {
2275                return TransitionResult::Failure {
2276                    message: format!("No correct count provided for pick-any on line {} with randomization activated. Computer says no...", line_cursor.sum_total()),
2277                    doctree: doctree
2278                };
2279            }
2280        } else {
2281            None
2282        },
2283        preserve_questions_between_attempts: if preserve_questions_between_attempts.is_some() {
2284            true
2285        } else {
2286            false
2287        },
2288    };
2289
2290    doctree = match doctree.push_data_and_focus(pick_any_node) {
2291        Ok(tree) => tree,
2292        Err(tree) => {
2293            return TransitionResult::Failure {
2294                message: format!(
2295                    "Node insertion error on line {}. Computer says no...",
2296                    line_cursor.sum_total()
2297                ),
2298                doctree: tree,
2299            }
2300        }
2301    };
2302
2303    // Check for assignment
2304
2305    Parser::skip_empty_lines(src_lines, line_cursor);
2306
2307    let start_line = match src_lines.get(line_cursor.relative_offset()) {
2308        Some(line) => line,
2309        None => return TransitionResult::Failure {
2310            message: format!(
2311                "Input overflow on line {} when parsing pick-any assignment. Computer says no...",
2312                line_cursor.sum_total()
2313            ),
2314            doctree: doctree
2315        }
2316    };
2317
2318    let assignment_inline_nodes: Vec<TreeNodeType> = if !CHOICE_RE.is_match(start_line) {
2319        let (block_lines, offset) = match Parser::read_text_block(src_lines, line_cursor.relative_offset(),  true, true, Some(body_indent), true) {
2320            TextBlockResult::Ok {lines, offset } => (lines, offset),
2321            TextBlockResult::Err {lines, offset } => return TransitionResult::Failure {
2322                message: format!("Could not read pick-any assignment lines starting on line {}. Computer says no...", line_cursor.sum_total()),
2323                doctree: doctree
2324            }
2325        };
2326
2327        let inline_nodes = match Parser::inline_parse(block_lines.join("\n"), None, line_cursor) {
2328            InlineParsingResult::Nodes(nodes) => nodes,
2329            _ => return TransitionResult::Failure {
2330                message: format!("Could not parse pick-any assignment for inline nodes on line {}. Computer says no...", line_cursor.sum_total()),
2331                doctree: doctree
2332            }
2333        };
2334
2335        line_cursor.increment_by(1);
2336
2337        inline_nodes
2338    } else {
2339        Vec::new()
2340    };
2341
2342    // Add assignment node (paragraph) to tree
2343
2344    if !assignment_inline_nodes.is_empty() {
2345        let assignment_node = TreeNodeType::Paragraph {
2346            indent: body_indent,
2347        };
2348        doctree = match doctree.push_data_and_focus(assignment_node) {
2349            Ok(tree) => tree,
2350            Err(tree) => {
2351                return TransitionResult::Failure {
2352                    message: format!(
2353                        "Node insertion error on line {}. Computer says no...",
2354                        line_cursor.sum_total()
2355                    ),
2356                    doctree: tree,
2357                }
2358            }
2359        };
2360        for node in assignment_inline_nodes {
2361            doctree = match doctree.push_data(node) {
2362                Ok(tree) => tree,
2363                Err(tree) => {
2364                    return TransitionResult::Failure {
2365                        message: format!(
2366                            "Node insertion error on line {}. Computer says no...",
2367                            line_cursor.sum_total()
2368                        ),
2369                        doctree: tree,
2370                    }
2371                }
2372            };
2373        }
2374        doctree = doctree.focus_on_parent()
2375    }
2376
2377    Parser::skip_empty_lines(src_lines, line_cursor);
2378
2379    // Read question choices
2380
2381    doctree = match doctree.push_data_and_focus(TreeNodeType::AplusPickChoices {
2382        body_indent: body_indent,
2383    }) {
2384        Ok(tree) => tree,
2385        Err(tree) => {
2386            return TransitionResult::Failure {
2387                message: format!(
2388                    "Node insertion error on line {}. Computer says no...",
2389                    line_cursor.sum_total()
2390                ),
2391                doctree: tree,
2392            }
2393        }
2394    };
2395
2396    while let Some(current_line) = src_lines.get(line_cursor.relative_offset()) {
2397        let indent = current_line
2398            .chars()
2399            .take_while(|c| c.is_whitespace())
2400            .count();
2401
2402        if indent != body_indent {
2403            break;
2404        }
2405
2406        let captures: regex::Captures = if let Some(capts) = CHOICE_RE.captures(current_line) {
2407            capts
2408        } else {
2409            break;
2410        };
2411
2412        let pre_selected = captures.name("pre_selected");
2413        let correct = captures.name("correct");
2414        let neutral = captures.name("neutral");
2415
2416        let label = captures.name("label").unwrap().as_str().to_string();
2417        let answer = if let Some(capture) = captures.name("answer") {
2418            capture.as_str()
2419        } else {
2420            ""
2421        };
2422
2423        if answer.trim().is_empty() {
2424            return TransitionResult::Failure {
2425                message: format!("Discovered a pick-any answer without content on line {}. Computer says no...", line_cursor.sum_total()),
2426                doctree: doctree
2427            };
2428        }
2429
2430        let answer_nodes: Vec<TreeNodeType> = match Parser::inline_parse(answer.to_string(), None, line_cursor) {
2431            InlineParsingResult::Nodes(nodes) => nodes,
2432            _ => return TransitionResult::Failure {
2433                message: format!("Could not parse pick-any answer on line {} for inline nodes. Computer says no...", line_cursor.sum_total()),
2434                doctree: doctree
2435            }
2436        };
2437
2438        let choice_node = TreeNodeType::AplusPickChoice {
2439            label: label,
2440            is_pre_selected: pre_selected.is_some(),
2441            is_correct: correct.is_some(),
2442            is_neutral: neutral.is_some(),
2443        };
2444
2445        doctree = match doctree.push_data_and_focus(choice_node) {
2446            Ok(tree) => tree,
2447            Err(tree) => {
2448                return TransitionResult::Failure {
2449                    message: format!(
2450                        "Node insertion error on line {}. Computer says no...",
2451                        line_cursor.sum_total()
2452                    ),
2453                    doctree: tree,
2454                }
2455            }
2456        };
2457        for node in answer_nodes {
2458            doctree = match doctree.push_data(node) {
2459                Ok(tree) => tree,
2460                Err(tree) => {
2461                    return TransitionResult::Failure {
2462                        message: format!(
2463                            "Node insertion error on line {}. Computer says no...",
2464                            line_cursor.sum_total()
2465                        ),
2466                        doctree: tree,
2467                    }
2468                }
2469            };
2470        }
2471        doctree = doctree.focus_on_parent();
2472
2473        line_cursor.increment_by(1);
2474    }
2475
2476    if doctree.n_of_children() == 0 {
2477        return TransitionResult::Failure {
2478            message: format!(
2479                "Found no choices for pick-any question on line {}. Computer says no...",
2480                line_cursor.sum_total()
2481            ),
2482            doctree: doctree,
2483        };
2484    }
2485
2486    doctree = doctree.focus_on_parent();
2487
2488    // Read possible hints inside the answers environment
2489
2490    Parser::skip_empty_lines(src_lines, line_cursor);
2491
2492    doctree = match doctree.push_data_and_focus(TreeNodeType::AplusQuestionnaireHints {
2493        body_indent: body_indent,
2494    }) {
2495        Ok(tree) => tree,
2496        Err(tree) => {
2497            return TransitionResult::Failure {
2498                message: format!(
2499                    "Node insertion error on line {}. Computer says no...",
2500                    line_cursor.sum_total()
2501                ),
2502                doctree: tree,
2503            }
2504        }
2505    };
2506
2507    while let Some(current_line) = src_lines.get(line_cursor.relative_offset()) {
2508        let indent = current_line
2509            .chars()
2510            .take_while(|c| c.is_whitespace())
2511            .count();
2512
2513        if indent != body_indent {
2514            break;
2515        }
2516
2517        let captures = if let Some(capts) = HINT_RE.captures(current_line) {
2518            capts
2519        } else {
2520            break;
2521        };
2522
2523        let show_not_answered = captures.name("show_not_answered");
2524        let label = match captures.name("label") {
2525            Some(enumerator) => enumerator.as_str().to_string(),
2526            None => {
2527                return TransitionResult::Failure {
2528                    message: format!(
2529                        "No label for pick-any hint on line {}. Computer says no...",
2530                        line_cursor.sum_total()
2531                    ),
2532                    doctree: doctree,
2533                }
2534            }
2535        };
2536        let hint: &str = if let Some(hint) = captures.name("hint") {
2537            hint.as_str().trim()
2538        } else {
2539            return TransitionResult::Failure {
2540                message: format!(
2541                    "No hint text for pick-any hint on line {}. Computer says no...",
2542                    line_cursor.sum_total()
2543                ),
2544                doctree: doctree,
2545            };
2546        };
2547
2548        if hint.is_empty() {
2549            return TransitionResult::Failure {
2550                message: format!(
2551                    "Empty hint text for hint on line {}. Computer says no...",
2552                    line_cursor.sum_total()
2553                ),
2554                doctree: doctree,
2555            };
2556        }
2557
2558        let hint_nodes: Vec<TreeNodeType> = match Parser::inline_parse(hint.to_string(), None, line_cursor) {
2559    InlineParsingResult::Nodes(nodes) => nodes,
2560    _ => return TransitionResult::Failure {
2561        message: format!("Could not parse pick-any answer on line {} for inline nodes. Computer says no...", line_cursor.sum_total()),
2562        doctree: doctree
2563    }
2564    };
2565
2566        if hint_nodes.is_empty() {
2567            return TransitionResult::Failure {
2568                message: format!(
2569                    "No inline nodes found for pick-any hint on line {}. Computer says no...",
2570                    line_cursor.sum_total()
2571                ),
2572                doctree: doctree,
2573            };
2574        }
2575
2576        let hint_node = TreeNodeType::AplusQuestionnaireHint {
2577            label: label,
2578            show_when_not_selected: show_not_answered.is_some(),
2579            question_type: crate::common::AplusQuestionnaireType::PickAny,
2580        };
2581
2582        doctree = match doctree.push_data_and_focus(hint_node) {
2583            Ok(tree) => tree,
2584            Err(tree) => {
2585                return TransitionResult::Failure {
2586                    message: format!(
2587                        "Node insertion error on line {}. Computer says no...",
2588                        line_cursor.sum_total()
2589                    ),
2590                    doctree: tree,
2591                }
2592            }
2593        };
2594        for node in hint_nodes {
2595            doctree = match doctree.push_data(node) {
2596                Ok(tree) => tree,
2597                Err(tree) => {
2598                    return TransitionResult::Failure {
2599                        message: format!(
2600                            "Node insertion error on line {}. Computer says no...",
2601                            line_cursor.sum_total()
2602                        ),
2603                        doctree: tree,
2604                    }
2605                }
2606            };
2607        }
2608        doctree = doctree.focus_on_parent();
2609
2610        line_cursor.increment_by(1);
2611    }
2612
2613    Parser::skip_empty_lines(src_lines, line_cursor);
2614
2615    doctree = doctree.focus_on_parent(); // Focus on pick-one
2616    doctree = doctree.focus_on_parent(); // Focus on questionnaire
2617
2618    // Return with modified doctree
2619
2620    TransitionResult::Success {
2621        doctree: doctree,
2622        push_or_pop: PushOrPop::Neither,
2623        line_advance: LineAdvance::None,
2624    }
2625}
2626
2627/// A `freetext` type questionnaire question parser.
2628pub fn parse_aplus_freetext(
2629    src_lines: &Vec<String>,
2630    mut doctree: DocTree,
2631    line_cursor: &mut LineCursor,
2632    first_indent: usize,
2633    body_indent: usize,
2634    empty_after_marker: bool,
2635) -> TransitionResult {
2636
2637    use crate::common::QuizPoints;
2638
2639    let (points, method_string) = if let Some(arg) = scan_directive_arguments(
2640        src_lines,
2641        line_cursor,
2642        body_indent,
2643        Some(first_indent),
2644        empty_after_marker,
2645    ) {
2646        let arg_string = arg.join(" ");
2647
2648        let mut arg_iter = arg_string.split_whitespace();
2649        let points_string: Option<&str> = arg_iter.next();
2650        let method_str = arg_iter.next();
2651
2652        let points: QuizPoints = if let Some(string) = points_string {
2653            if let Ok(result) = string.parse() {
2654                result
2655            } else {
2656                return TransitionResult::Failure {
2657                    message: format!("Quiz freetext question points preceding line {} could not be parsed into an integer. Computer says no...", line_cursor.sum_total()),
2658                    doctree: doctree
2659                };
2660            }
2661        } else {
2662            return TransitionResult::Failure {
2663                message: format!(
2664                    "No points found for freetext on line {}. Computer says no...",
2665                    line_cursor.sum_total()
2666                ),
2667                doctree: doctree,
2668            };
2669        };
2670        let method_string = if let Some(string) = method_str {
2671            string.to_string()
2672        } else {
2673            String::new()
2674        };
2675
2676        (points, method_string)
2677    } else {
2678        return TransitionResult::Failure {
2679            message: format!(
2680                "No points provided for freetext question on line {}. Computer says no...",
2681                line_cursor.sum_total()
2682            ),
2683            doctree: doctree,
2684        };
2685    };
2686
2687    if let TreeNodeType::AplusQuestionnaire {
2688        points_from_children,
2689        ..
2690    } = doctree.mut_node_data()
2691    {
2692        *points_from_children += points;
2693    }
2694
2695    let (class, required, key, length, height) = if let Some(mut options) = scan_directive_options
2696    (src_lines, line_cursor, body_indent) {
2697        (
2698            options.remove("class"),
2699            options.remove("required"),
2700            options.remove("key"),
2701            options.remove("length"),
2702            options.remove("height"),
2703        )
2704    } else {
2705        (None, None, None, None, None)
2706    };
2707
2708    let freetext_node = TreeNodeType::AplusFreeText {
2709        body_indent: body_indent,
2710        points: points,
2711        compare_method: method_string,
2712        model_answer: String::new(),
2713        class: class,
2714        required: required,
2715        key: key,
2716        length: length,
2717        height: height,
2718    };
2719
2720    doctree = match doctree.push_data_and_focus(freetext_node) {
2721        Ok(tree) => tree,
2722        Err(tree) => {
2723            return TransitionResult::Failure {
2724                message: format!(
2725                    "Node insertion error on line {}. Computer says no...",
2726                    line_cursor.sum_total()
2727                ),
2728                doctree: tree,
2729            }
2730        }
2731    };
2732
2733    Parser::skip_empty_lines(src_lines, line_cursor);
2734
2735    // Read in assignment
2736
2737    let assignment_inline_nodes: Vec<TreeNodeType> = {
2738        let (block_lines, offset) = match Parser::read_text_block(src_lines, line_cursor.relative_offset(),  true, true, Some(body_indent), true) {
2739            TextBlockResult::Ok { lines, offset } => (lines, offset),
2740            TextBlockResult::Err { lines, offset } => return TransitionResult::Failure {
2741                message: format!("Could not read pick-any assignment lines starting on line {}. Computer says no...", line_cursor.sum_total()),
2742                doctree: doctree
2743            }
2744        };
2745
2746        let inline_nodes = match Parser::inline_parse(block_lines.join("\n"), None, line_cursor) {
2747            InlineParsingResult::Nodes(nodes) => nodes,
2748            _ => return TransitionResult::Failure {
2749                message: format!("Could not parse pick-any assignment for inline nodes on line {}. Computer says no...", line_cursor.sum_total()),
2750                doctree: doctree
2751            }
2752        };
2753
2754        line_cursor.increment_by(1);
2755
2756        inline_nodes
2757    };
2758
2759    // Add assignment node (paragraph) to tree
2760
2761    let assignment_node = TreeNodeType::Paragraph {
2762        indent: body_indent,
2763    };
2764    doctree = match doctree.push_data_and_focus(assignment_node) {
2765        Ok(tree) => tree,
2766        Err(tree) => {
2767            return TransitionResult::Failure {
2768                message: format!(
2769                    "Node insertion error on line {}. Computer says no...",
2770                    line_cursor.sum_total()
2771                ),
2772                doctree: tree,
2773            }
2774        }
2775    };
2776    for node in assignment_inline_nodes {
2777        doctree = match doctree.push_data(node) {
2778            Ok(tree) => tree,
2779            Err(tree) => {
2780                return TransitionResult::Failure {
2781                    message: format!(
2782                        "Node insertion error on line {}. Computer says no...",
2783                        line_cursor.sum_total()
2784                    ),
2785                    doctree: tree,
2786                }
2787            }
2788        };
2789    }
2790    doctree = doctree.focus_on_parent();
2791
2792    Parser::skip_empty_lines(src_lines, line_cursor);
2793
2794    // Read in model answer
2795
2796    if let Some(answer) = src_lines.get(line_cursor.relative_offset()) {
2797        let indent = answer.chars().take_while(|c| c.is_whitespace()).count();
2798        if indent != body_indent {
2799            return TransitionResult::Failure {
2800                message: format!("A+ freetext answer has incorrect indentation on line {}. Computer says no...", line_cursor.sum_total()),
2801                doctree: doctree
2802            };
2803        }
2804
2805        if let TreeNodeType::AplusFreeText { model_answer, .. } = doctree.mut_node_data() {
2806            model_answer.push_str(answer.trim());
2807        } else {
2808            return TransitionResult::Failure {
2809                message: format!("Not focused on A+ freetext node when reading its model answer on line {}? Computer says no...", line_cursor.sum_total()),
2810                doctree: doctree
2811            };
2812        }
2813
2814        line_cursor.increment_by(1);
2815    } else {
2816        return TransitionResult::Failure {
2817            message: format!("Tried scanning freetext question for correct answer but encountered end of input on line {}. Computer says no...", line_cursor.sum_total()),
2818            doctree: doctree
2819        };
2820    };
2821
2822    // Read possible hints
2823    const APLUS_PICK_HINT_PATTERN: &'static str =
2824        r"^(\s*)(?P<show_not_answered>!)?(?P<label>.+)[ ]*§[ ]*(?P<hint>.+)";
2825    lazy_static::lazy_static! {
2826        static ref HINT_RE: regex::Regex = regex::Regex::new(APLUS_PICK_HINT_PATTERN).unwrap();
2827    }
2828
2829    Parser::skip_empty_lines(src_lines, line_cursor);
2830
2831    doctree = match doctree.push_data_and_focus(
2832        TreeNodeType::AplusQuestionnaireHints {
2833            body_indent: body_indent,
2834        }
2835    ) {
2836        Ok(tree) => tree,
2837        Err(tree) => {
2838            return TransitionResult::Failure {
2839                message: format!(
2840                    "Node insertion error on line {}. Computer says no...",
2841                    line_cursor.sum_total()
2842                ),
2843                doctree: tree,
2844            }
2845        }
2846    };
2847
2848    while let Some(current_line) = src_lines.get(line_cursor.relative_offset()) {
2849        let indent = current_line
2850            .chars()
2851            .take_while(|c| c.is_whitespace())
2852            .count();
2853
2854        if indent != body_indent {
2855            break;
2856        }
2857
2858        let captures = if let Some(capts) = HINT_RE.captures(current_line) {
2859            capts
2860        } else {
2861            break;
2862        };
2863
2864        let show_not_answered = captures.name("show_not_answered");
2865        let label = match captures.name("label") {
2866            Some(label) => label.as_str().trim().to_string(),
2867            None => {
2868                return TransitionResult::Failure {
2869                    message: format!(
2870                        "No text for freetext hint on line {}. Computer says no...",
2871                        line_cursor.sum_total()
2872                    ),
2873                    doctree: doctree,
2874                }
2875            }
2876        };
2877        let hint: &str = if let Some(hint) = captures.name("hint") {
2878            hint.as_str().trim()
2879        } else {
2880            return TransitionResult::Failure {
2881                message: format!(
2882                    "No hint text for freetext hint on line {}. Computer says no...",
2883                    line_cursor.sum_total()
2884                ),
2885                doctree: doctree,
2886            };
2887        };
2888
2889        if hint.is_empty() {
2890            return TransitionResult::Failure {
2891                message: format!(
2892                    "Empty hint text for hint on line {}. Computer says no...",
2893                    line_cursor.sum_total()
2894                ),
2895                doctree: doctree,
2896            };
2897        }
2898
2899        let hint_nodes: Vec<TreeNodeType> = match Parser::inline_parse(hint.to_string(), None, line_cursor) {
2900            InlineParsingResult::Nodes(nodes) => nodes,
2901            _ => return TransitionResult::Failure {
2902                message: format!("Could not parse freetext hint on line {} for inline nodes. Computer says no...", line_cursor.sum_total()),
2903                doctree: doctree
2904            }
2905        };
2906
2907        if hint_nodes.is_empty() {
2908            return TransitionResult::Failure {
2909                message: format!(
2910                    "No inline nodes found for freetext hint on line {}. Computer says no...",
2911                    line_cursor.sum_total()
2912                ),
2913                doctree: doctree,
2914            };
2915        }
2916
2917        let hint_node = TreeNodeType::AplusQuestionnaireHint {
2918            label: label,
2919            show_when_not_selected: show_not_answered.is_some(),
2920            question_type: crate::common::AplusQuestionnaireType::FreeText,
2921        };
2922
2923        doctree = match doctree.push_data_and_focus(hint_node) {
2924            Ok(tree) => tree,
2925            Err(tree) => {
2926                return TransitionResult::Failure {
2927                    message: format!(
2928                        "Node insertion error on line {}. Computer says no...",
2929                        line_cursor.sum_total()
2930                    ),
2931                    doctree: tree,
2932                }
2933            }
2934        };
2935        for node in hint_nodes {
2936            doctree = match doctree.push_data(node) {
2937                Ok(tree) => tree,
2938                Err(tree) => {
2939                    return TransitionResult::Failure {
2940                        message: format!(
2941                            "Node insertion error on line {}. Computer says no...",
2942                            line_cursor.sum_total()
2943                        ),
2944                        doctree: tree,
2945                    }
2946                }
2947            };
2948        }
2949        doctree = doctree.focus_on_parent();
2950
2951        line_cursor.increment_by(1);
2952    }
2953
2954    Parser::skip_empty_lines(src_lines, line_cursor);
2955
2956    doctree = doctree.focus_on_parent(); // Focus on pick-one
2957    doctree = doctree.focus_on_parent(); // Focus on questionnaire
2958
2959    TransitionResult::Success {
2960        doctree: doctree,
2961        push_or_pop: PushOrPop::Neither,
2962        line_advance: LineAdvance::None,
2963    }
2964}
2965
2966pub fn parse_aplus_submit(
2967    src_lines: &Vec<String>,
2968    mut doctree: DocTree,
2969    line_cursor: &mut LineCursor,
2970    first_indent: usize,
2971    body_indent: usize,
2972    empty_after_marker: bool,
2973) -> TransitionResult {
2974
2975    let (key, difficulty, max_points): (String, String, String) = if let Some(arg) =
2976        scan_directive_arguments(
2977            src_lines,
2978            line_cursor,
2979            body_indent,
2980            Some(first_indent),
2981            empty_after_marker,
2982        ) {
2983        aplus_key_difficulty_and_max_points(arg.join(" ").as_str(), line_cursor)
2984    } else {
2985        return TransitionResult::Failure {
2986            message: format!(
2987                "A+ submit exercise on line {} was not given arguments. Computer says no...",
2988                line_cursor.sum_total()
2989            ),
2990            doctree: doctree,
2991        };
2992    };
2993
2994    Parser::skip_empty_lines(src_lines, line_cursor);
2995
2996    let (
2997        config,
2998        submissions,
2999        points_to_pass,
3000        class,
3001        title,
3002        category,
3003        status,
3004        ajax,
3005        allow_assistant_viewing,
3006        allow_assistant_grading,
3007        quiz,
3008        url,
3009        radar_tokenizer,
3010        radar_minimum_match_tokens,
3011        lti,
3012        lti_resource_link_id,
3013        lti_open_in_iframe,
3014        lti_aplus_get_and_post,
3015    ) = if let Some(mut options) = scan_directive_options(
3016        src_lines, line_cursor, body_indent
3017    ) {
3018        (
3019            options.remove("config"),
3020            options.remove("submissions"),
3021            options.remove("points-to-pass"),
3022            options.remove("class"),
3023            options.remove("title"),
3024            options.remove("category"),
3025            options.remove("status"),
3026            options.remove("ajax"),
3027            options.remove("allow-assistant-viewing"),
3028            options.remove("allow-assistant-grading"),
3029            options.remove("quiz"),
3030            options.remove("url"),
3031            options.remove("radar-tokenizer"),
3032            options.remove("radar_minimum_match_tokens"),
3033            options.remove("lti"),
3034            options.remove("lti_resource_link_id"),
3035            options.remove("lti_open_in_iframe"),
3036            options.remove("lti_aplus_get_and_post"),
3037        )
3038    } else {
3039        (
3040            None, None, None, None, None, None, None, None, None, None, None, None, None, None,
3041            None, None, None, None,
3042        )
3043    };
3044
3045    if config.is_none() {
3046        return TransitionResult::Failure {
3047    message: format!("A+ submit exercise on line {} has to specify a configuration file location via the :config: option. Computer says no...", line_cursor.sum_total()),
3048    doctree: doctree
3049    };
3050    }
3051
3052    // Unpacking some options
3053    let max_points = if let Ok(result) = max_points.parse() {
3054        result
3055    } else {
3056        10
3057    };
3058    let points_to_pass = if let Some(ptp) = points_to_pass {
3059        if let Ok(result) = ptp.parse() {
3060            result
3061        } else {
3062            0
3063        }
3064    } else {
3065        0
3066    };
3067
3068    use crate::common::AplusExerciseStatus;
3069    let status = if let Some(status) = status {
3070        match status.as_str().trim() {
3071            "ready" => AplusExerciseStatus::Ready,
3072            "unlisted" => AplusExerciseStatus::Unlisted,
3073            "hidden" => AplusExerciseStatus::Hidden,
3074            "enrollment" => AplusExerciseStatus::Enrollment,
3075            "enrollment_ext" => AplusExerciseStatus::EnrollmentExt,
3076            "maintenance" => AplusExerciseStatus::Maintenance,
3077            _ => AplusExerciseStatus::Unlisted,
3078        }
3079    } else {
3080        AplusExerciseStatus::Unlisted // Default
3081    };
3082
3083    use crate::common::AplusRadarTokenizer;
3084    let tokenizer =
3085        if let Some(tokenizer) = radar_tokenizer {
3086            match tokenizer.as_str().trim() {
3087                "python" => AplusRadarTokenizer::Python3,
3088                "scala" => AplusRadarTokenizer::Scala,
3089                "javascript" => AplusRadarTokenizer::JavaScript,
3090                "css" => AplusRadarTokenizer::CSS,
3091                "html" => AplusRadarTokenizer::HTML,
3092                _ => return TransitionResult::Failure {
3093                    message: format!(
3094                        "No such tokenizer A+ submit exerciose on line {}. Computer says no...",
3095                        line_cursor.sum_total()
3096                    ),
3097                    doctree: doctree,
3098                },
3099            }
3100        } else {
3101            AplusRadarTokenizer::None // Default
3102        };
3103
3104    let lti = if let Some(lti) = lti {
3105        lti
3106    } else {
3107        String::new()
3108    };
3109
3110    // Crate submit node
3111
3112    let submit_node = TreeNodeType::AplusSubmit {
3113        body_indent: body_indent,
3114        key: key,
3115        difficulty: difficulty,
3116        max_points: max_points,
3117        config: config.unwrap(),
3118        submissions: if let Some(submissions) = submissions {
3119            if let Ok(result) = submissions.parse() {
3120                result
3121            } else {
3122                10
3123            }
3124        } else {
3125            10
3126        },
3127        points_to_pass: points_to_pass,
3128        class: if let Some(class) = class {
3129            class
3130        } else {
3131            String::new()
3132        },
3133        title: if let Some(title) = title {
3134            title
3135        } else {
3136            String::new()
3137        },
3138        category: if let Some(category) = category {
3139            category
3140        } else {
3141            String::from("submit")
3142        },
3143        status: status,
3144        ajax: ajax.is_some(),
3145        allow_assistant_viewing: allow_assistant_viewing.is_some(),
3146        allow_assistant_grading: allow_assistant_grading.is_some(),
3147        quiz: quiz.is_some(),
3148        url: if let Some(url) = url {
3149            url
3150        } else {
3151            String::new()
3152        },
3153        radar_tokenizer: tokenizer, // implements Copy, so can be used below
3154        radar_minimum_match_tokens: if let Some(min) = radar_minimum_match_tokens {
3155            if let AplusRadarTokenizer::None = tokenizer {
3156                None
3157            } else {
3158                if let Ok(result) = min.parse() {
3159                    Some(result)
3160                } else {
3161                    None
3162                }
3163            }
3164        } else {
3165            None
3166        },
3167        lti: lti,
3168        lti_resource_link_id: if let Some(id) = lti_resource_link_id {
3169            id
3170        } else {
3171            String::new()
3172        },
3173        lti_open_in_iframe: lti_open_in_iframe.is_some(),
3174        lti_aplus_get_and_post: lti_aplus_get_and_post.is_some(),
3175    };
3176
3177    doctree = match doctree.push_data_and_focus(submit_node) {
3178        Ok(tree) => tree,
3179        Err(tree) => {
3180            return TransitionResult::Failure {
3181                message: format!(
3182                    "Node insertion error on line {}. Computer says no...",
3183                    line_cursor.sum_total()
3184                ),
3185                doctree: tree,
3186            }
3187        }
3188    };
3189
3190    TransitionResult::Success {
3191        doctree: doctree,
3192        push_or_pop: PushOrPop::Push(vec![State::Body]),
3193        line_advance: LineAdvance::None,
3194    }
3195}
3196
3197pub fn parse_aplus_toctree() {
3198    todo!()
3199}
3200
3201pub fn parse_aplus_active_element_input(
3202    src_lines: &Vec<String>,
3203    mut doctree: DocTree,
3204    line_cursor: &mut LineCursor,
3205    base_indent: usize,
3206    empty_after_marker: bool,
3207    first_indent: usize,
3208    body_indent: usize,
3209) -> TransitionResult {
3210
3211    let key_for_input = if let Some(args) = scan_directive_arguments(
3212        src_lines,
3213        line_cursor,
3214        body_indent,
3215        Some(first_indent),
3216        empty_after_marker,
3217    ) {
3218        args.join(" ")
3219    } else {
3220        return TransitionResult::Failure {
3221    message: format!("A+ active element input before line {} has no key for output. Computer says no...", line_cursor.sum_total()),
3222    doctree: doctree
3223    };
3224    };
3225
3226    let (title, default, class, width, height, clear, input_type, file) =
3227        if let Some(mut options) = scan_directive_options
3228        (src_lines, line_cursor, body_indent) {
3229            (
3230                options.remove("title"),
3231                options.remove("default"),
3232                options.remove("class"),
3233                options.remove("width"),
3234                options.remove("height"),
3235                options.remove("clear"),
3236                options.remove("type"),
3237                options.remove("file"),
3238            )
3239        } else {
3240            (None, None, None, None, None, None, None, None)
3241        };
3242
3243    use crate::common::{AplusActiveElementClear, AplusActiveElementInputType};
3244
3245    let ae_input_node = TreeNodeType::AplusActiveElementInput {
3246        key_for_input: key_for_input,
3247        title: title,
3248        default: default,
3249        class: class,
3250        width: if let Some(w) = &width {
3251            converters::str_to_length(w)
3252        } else {
3253            None
3254        },
3255        height: if let Some(h) = &height {
3256            converters::str_to_length(h)
3257        } else {
3258            None
3259        },
3260        clear: if let Some(clear) = clear {
3261            match clear.as_str() {
3262                "both" => Some(AplusActiveElementClear::Both),
3263                "left" => Some(AplusActiveElementClear::Left),
3264                "right" => Some(AplusActiveElementClear::Right),
3265                _ => return TransitionResult::Failure {
3266                    message: format!("No such clear type for A+ active element input before line {}. Computer says no...", line_cursor.sum_total()),
3267                    doctree: doctree
3268                }
3269            }
3270        } else {
3271            None
3272        },
3273        input_type: if let Some(input_type) = &input_type {
3274            if input_type == "file" {
3275                Some(AplusActiveElementInputType::File)
3276            } else if input_type == "clickable" {
3277                Some(AplusActiveElementInputType::Clickable)
3278            } else if input_type.starts_with("dropdown:") {
3279                let options = if let Some(options) = input_type.split(":").last() {
3280                    options
3281                } else {
3282                    return TransitionResult::Failure {
3283                        message: format!("No options for dropdown input for A+ activ element input before line {}. Computer says no...", line_cursor.sum_total()),
3284                        doctree: doctree
3285                    };
3286                };
3287                Some(crate::common::AplusActiveElementInputType::Dropdown(
3288                    options.to_string(),
3289                ))
3290            } else {
3291                return TransitionResult::Failure {
3292                    message: format!("No such input type for A+ active element input before line {}. Ignoring...", line_cursor.sum_total()),
3293                    doctree: doctree
3294                };
3295            }
3296        } else {
3297            None
3298        },
3299        file: if let (Some(input_type), Some(file)) = (input_type, file) {
3300            if input_type == "clickable" {
3301                Some(file)
3302            } else {
3303                None
3304            }
3305        } else {
3306            None
3307        },
3308    };
3309
3310    doctree = match doctree.push_data(ae_input_node) {
3311        Ok(tree) => tree,
3312        Err(tree) => {
3313            return TransitionResult::Failure {
3314                message: format!(
3315                    "Node insertion error on line {}. Computer says no...",
3316                    line_cursor.sum_total()
3317                ),
3318                doctree: tree,
3319            }
3320        }
3321    };
3322
3323    TransitionResult::Success {
3324        doctree: doctree,
3325        push_or_pop: PushOrPop::Neither,
3326        line_advance: LineAdvance::None,
3327    }
3328}
3329
3330/// Parses an A+ active element output directive into the respective node
3331pub fn parse_aplus_active_element_output(
3332    src_lines: &Vec<String>,
3333    mut doctree: DocTree,
3334    line_cursor: &mut LineCursor,
3335    base_indent: usize,
3336    empty_after_marker: bool,
3337    first_indent: usize,
3338    body_indent: usize,
3339) -> TransitionResult {
3340
3341    let key_for_output = if let Some(args) = scan_directive_arguments(
3342        src_lines,
3343        line_cursor,
3344        body_indent,
3345        Some(first_indent),
3346        empty_after_marker,
3347    ) {
3348        args.join(" ")
3349    } else {
3350        return TransitionResult::Failure {
3351            message: format!("A+ active element output before line {} has no key for output. Computer says no...", line_cursor.sum_total()),
3352            doctree: doctree
3353        };
3354    };
3355
3356    let (
3357        config,
3358        inputs,
3359        title,
3360        class,
3361        width,
3362        height,
3363        clear,
3364        output_type,
3365        file,
3366        submissions,
3367        scale_size,
3368        status,
3369    ) = if let Some(mut options) = scan_directive_options(
3370        src_lines, line_cursor, body_indent
3371    ) {
3372        (
3373            options.remove("config"),
3374            options.remove("inputs"),
3375            options.remove("title"),
3376            options.remove("class"),
3377            options.remove("width"),
3378            options.remove("height"),
3379            options.remove("clear"),
3380            options.remove("type"),
3381            options.remove("file"),
3382            options.remove("submissions"),
3383            options.remove("scale-size"),
3384            options.remove("status")
3385        )
3386    } else {
3387        (
3388            None, None, None, None, None, None, None, None, None, None, None, None
3389        )
3390    };
3391
3392    use crate::common::{
3393        AplusActiveElementClear, AplusActiveElementOutputType, AplusExerciseStatus,
3394    };
3395
3396    let ae_output_node = TreeNodeType::AplusActiveElementOutput {
3397        key_for_output: key_for_output,
3398        config: if let Some(config) = config {
3399            config
3400        } else {
3401            return TransitionResult::Failure {
3402                message: format!("A+ active element output before line {} must have a set config file via the \"config\" option. Computer says no...", line_cursor.sum_total()),
3403                doctree: doctree
3404            };
3405        },
3406        inputs: if let Some(inputs) = inputs {
3407            inputs
3408        } else {
3409            return TransitionResult::Failure {
3410                message: format!("A+ active element output before line {} must have a set of inputs set via the \"inputs\" setting. Computer says no...", line_cursor.sum_total()),
3411                doctree: doctree
3412            };
3413        },
3414        title: title,
3415        class: class,
3416        width: if let Some(w) = &width {
3417            converters::str_to_length(w)
3418        } else {
3419            None
3420        },
3421        height: if let Some(h) = &height {
3422            converters::str_to_length(h)
3423        } else {
3424            None
3425        },
3426        clear: if let Some(clear) = clear {
3427            match clear.as_str() {
3428                "both"  => Some(AplusActiveElementClear::Both),
3429                "left"  => Some(AplusActiveElementClear::Left),
3430                "right" => Some(AplusActiveElementClear::Right),
3431                _       => None
3432            }
3433        } else {
3434            None
3435        },
3436        output_type: if let Some(output_type) = output_type {
3437            match output_type.as_str() {
3438                "text"  => AplusActiveElementOutputType::Text,
3439                "image" => AplusActiveElementOutputType::Image,
3440                _       => AplusActiveElementOutputType::Text
3441            }
3442        } else {
3443            AplusActiveElementOutputType::Text
3444        },
3445        submissions: if let Some(submissions) = submissions {
3446            if let Ok(result) = submissions.parse::<u32>() {
3447                Some(result)
3448            } else {
3449                None
3450            }
3451        } else {
3452            None
3453        },
3454        scale_size: if let Some(_) = scale_size {
3455            true
3456        } else {
3457            false
3458        },
3459        status: if let Some(status) = status {
3460            match status.as_str().trim() {
3461                "ready"     => AplusExerciseStatus::Ready,
3462                "unlisted"  => AplusExerciseStatus::Unlisted,
3463                "hidden"    => AplusExerciseStatus::Hidden,
3464                "enrollment"        => AplusExerciseStatus::Enrollment,
3465                "enrollment_ext"    => AplusExerciseStatus::EnrollmentExt,
3466                "maintenance"       => AplusExerciseStatus::Maintenance,
3467                _                   => AplusExerciseStatus::Unlisted
3468            }
3469        } else {
3470            AplusExerciseStatus::Unlisted
3471        },
3472    };
3473
3474    doctree = match doctree.push_data(ae_output_node) {
3475        Ok(tree) => tree,
3476        Err(tree) => {
3477            return TransitionResult::Failure {
3478                message: format!(
3479                    "Node insertion error on line {}. Computer says no...",
3480                    line_cursor.sum_total()
3481                ),
3482                doctree: tree,
3483            }
3484        }
3485    };
3486
3487    TransitionResult::Success {
3488        doctree: doctree,
3489        push_or_pop: PushOrPop::Neither,
3490        line_advance: LineAdvance::None,
3491    }
3492}
3493
3494pub fn parse_aplus_hidden_block() {
3495    todo!()
3496}
3497
3498/// Parses an A+ point of interest directive into the respective node.
3499
3500
3501/// Add support for the row and column directives introduced in November 2020.
3502pub fn parse_aplus_point_of_interest(
3503    src_lines: &Vec<String>,
3504    mut doctree: DocTree,
3505    line_cursor: &mut LineCursor,
3506    base_indent: usize,
3507    empty_after_marker: bool,
3508    first_indent: usize,
3509    body_indent: usize,
3510    section_level: usize,
3511) -> TransitionResult {
3512
3513    let title = scan_directive_arguments(
3514        src_lines,
3515        line_cursor,
3516        body_indent,
3517        Some(first_indent),
3518        empty_after_marker,
3519    );
3520
3521    // Read recognized options
3522    let (
3523        id,
3524        previous,
3525        next,
3526        hidden,
3527        class,
3528        height,
3529        columns,
3530        bgimg,
3531        not_in_slides,
3532        not_in_book,
3533        no_poi_box,
3534    ) = if let Some(mut options) = scan_directive_options(
3535        src_lines, line_cursor, body_indent
3536    ) {
3537
3538        (
3539            options.remove("id"),
3540            options.remove("previous"),
3541            options.remove("next"),
3542            options.remove("hidden"),
3543            options.remove("class"),
3544            options.remove("height"),
3545            options.remove("columns"),
3546            options.remove("bgimg"),
3547            options.remove("not_in_slides"),
3548            options.remove("not_in_book"),
3549            options.remove("no_poi_box"),
3550        )
3551    } else {
3552        (
3553            None, None, None, None, None, None, None, None, None, None, None,
3554        )
3555    };
3556
3557    let poi_node = TreeNodeType::AplusPOI {
3558        title: if let Some(title) = title {
3559            title.join(" ")
3560        } else {
3561            "".to_string()
3562        },
3563        body_indent: body_indent,
3564
3565        id: id,
3566        previous: previous,
3567        next: next,
3568        hidden: hidden,
3569        class: class,
3570        height: if let Some(h) = &height {
3571            converters::str_to_length(h)
3572        } else {
3573            None
3574        },
3575        columns: columns,
3576        bgimg: bgimg,
3577        not_in_slides: not_in_slides,
3578        not_in_book: not_in_book,
3579        no_poi_box: no_poi_box,
3580    };
3581
3582    doctree = match doctree.push_data_and_focus(poi_node) {
3583        Ok(tree) => tree,
3584        Err(tree) => {
3585            return TransitionResult::Failure {
3586                message: format!(
3587                    "Node insertion error on line {}. Computer says no...",
3588                    line_cursor.sum_total()
3589                ),
3590                doctree: tree,
3591            }
3592        }
3593    };
3594
3595    TransitionResult::Success {
3596        doctree: doctree,
3597        push_or_pop: PushOrPop::Push(vec![State::AplusMultiCol]), // PoI contains body nodes and A+ specific column breaks
3598        line_advance: LineAdvance::None,
3599    }
3600}
3601
3602pub fn parse_aplus_annotated() {
3603    todo!()
3604}
3605
3606pub fn parse_aplus_lineref_codeblock() {
3607    todo!()
3608}
3609
3610pub fn parse_aplus_repl_res_count_reset() {
3611    todo!()
3612}
3613
3614pub fn parse_aplus_acos_submit() {
3615    todo!()
3616}
3617
3618pub fn parse_aplus_div() {
3619    todo!()
3620}
3621
3622pub fn parse_aplus_styled_topic() {
3623    todo!()
3624}
3625
3626pub fn parse_aplus_story() {
3627    todo!()
3628}
3629
3630pub fn parse_aplus_jsvee() {
3631    todo!()
3632}
3633
3634pub fn parse_aplus_youtube() {
3635    todo!()
3636}
3637
3638pub fn parse_aplus_local_video() {
3639    todo!()
3640}
3641
3642pub fn parse_aplus_embedded_page() {
3643    todo!()
3644}
3645
3646/// Parses unknown directive blocks as literal text.
3647pub fn parse_unknown_directive(
3648    mut doctree: DocTree,
3649    src_lines: &Vec<String>,
3650    line_cursor: &mut LineCursor,
3651    directive_name: &str,
3652    first_line_indent: usize,
3653    body_indent: usize,
3654    empty_after_marker: bool,
3655) -> TransitionResult {
3656    let argument = if let Some(arg) = scan_directive_arguments(
3657        src_lines,
3658        line_cursor,
3659        body_indent,
3660        Some(first_line_indent),
3661        empty_after_marker,
3662    ) {
3663        arg.join(" ").trim().to_string()
3664    } else {
3665        String::new()
3666    };
3667
3668    let options = if let Some(options) =
3669        scan_directive_options
3670        (src_lines, line_cursor, body_indent)
3671    {
3672        options
3673    } else {
3674        HashMap::new()
3675    };
3676
3677    let unknown_directive_data = TreeNodeType::UnknownDirective {
3678        directive_name: String::from(directive_name),
3679        argument: argument,
3680        options: options,
3681        body_indent: body_indent,
3682    };
3683
3684    doctree = match doctree.push_data_and_focus(unknown_directive_data) {
3685        Ok(tree) => tree,
3686        Err(tree) => {
3687            return TransitionResult::Failure {
3688                message: format!(
3689                    "Could not add unknown directive data to doctree on line {}",
3690                    line_cursor.sum_total()
3691                ),
3692                doctree: tree,
3693            }
3694        }
3695    };
3696
3697    TransitionResult::Success {
3698        doctree: doctree,
3699        push_or_pop: PushOrPop::Push(vec![State::Body]),
3700        line_advance: LineAdvance::None,
3701    }
3702
3703    // let (unknown_directive_as_text, offset) = match Parser::read_indented_block(src_lines, Some(line_cursor.relative_offset()), Some(false), Some(false), Some(body_indent), Some(first_line_indent), false) {
3704    //   Ok((lines, _, offset, _)) => {
3705    //     (lines.join("\n"), offset)
3706    //   },
3707    //   Err(message) => return TransitionResult::Failure {
3708    //     message: format!("Error when reading an unknown directive as literal text: {}", message),
3709    //     doctree: doctree
3710    //   }
3711    // };
3712
3713    // let literal_node = TreeNodeType::LiteralBlock { text: unknown_directive_as_text };
3714
3715    // doctree = match doctree.push_data(literal_node) {
3716    //   Ok(tree) => tree,
3717    //   Err(tree) => return TransitionResult::Failure {
3718    //     message: format!("Node insertion error on line {}. Computer says no...", line_cursor.sum_total()),
3719    //     doctree: tree
3720    //   }
3721    // };
3722
3723    // TransitionResult::Success {
3724    //   doctree: doctree,
3725    //   next_states: None,
3726    //   push_or_pop: PushOrPop::Neither,
3727    //   line_advance: LineAdvance::Some(offset)
3728    // }
3729}
3730
3731// ---------
3732//  HELPERS
3733// ---------
3734
3735/// Reads the first block of text of a directive,
3736/// until an empty line or something like a list of options
3737/// (recognized by the automaton `FIELD_MAKRER_RE`) is encountered.
3738/// Combines the lines into a single string and `Option`ally returns it.
3739/// If no arguments are found, returns `None`.
3740
3741/// In case the directive starts on the same line as the directive marker,
3742/// allows specifying first and block indents separately.
3743/// `first_indent` (on the first line) or `block_indent` are ignored on each line.
3744fn scan_directive_arguments(
3745    src_lines: &Vec<String>,
3746    line_cursor: &mut LineCursor,
3747    body_indent: usize,
3748    first_indent: Option<usize>,
3749    empty_after_marker: bool,
3750) -> Option<Vec<String>> {
3751    use crate::parser::automata::FIELD_MARKER_AUTOMATON;
3752
3753    // The vector containing references to the argument lines.
3754    let mut argument_lines: Vec<String> = Vec::new();
3755    let mut on_marker_line = true;
3756
3757    // Jump to next line if line after directive marker is empty
3758    if empty_after_marker {
3759        line_cursor.increment_by(1);
3760        on_marker_line = false;
3761    }
3762
3763    while let Some(line) = src_lines.get(line_cursor.relative_offset()) {
3764
3765
3766        // Each collect allocates, but what the heck, it works.
3767        let line_without_indent: String = if on_marker_line {
3768            match first_indent {
3769                Some(indent) => {
3770                    on_marker_line = false;
3771                    line.chars().skip(indent).collect()
3772                }
3773                None => panic!("On directive marker line {} but couldn't skip the marker to parse line contents. Computer says no...", line_cursor.sum_total())
3774            }
3775        } else {
3776            // Check for proper indentation
3777            if ! (line.chars().take_while(|c| c.is_whitespace()).count() == body_indent) {
3778                break
3779            };
3780            line.chars()
3781                .skip_while(|c| c.is_whitespace())
3782                .collect::<String>()
3783                .as_str()
3784                .trim()
3785                .to_string()
3786        };
3787
3788        if line_without_indent.as_str().trim().is_empty()
3789            || FIELD_MARKER_AUTOMATON.is_match(line_without_indent.as_str())
3790        {
3791            break;
3792        }
3793
3794        argument_lines.push(line_without_indent);
3795        line_cursor.increment_by(1);
3796    }
3797
3798    if argument_lines.is_empty() {
3799        None
3800    } else {
3801        Some(argument_lines)
3802    }
3803}
3804
3805
3806/// Scans the lines following the directive marker for something resembling a field list,
3807/// and attempts to scan the contents of the list into an `Option`al `HashMap` of directive
3808/// option names and values. The calling directive parser will handle their validation,
3809/// as different directives have different options available to them.
3810
3811/// An empty line separates directive options from the directive content, so encountering one
3812/// will terminate the scan. This means that the options have to start of the line following
3813/// the directive marker.
3814fn scan_directive_options(
3815    src_lines: &Vec<String>,
3816    line_cursor: &mut LineCursor,
3817    body_indent: usize,
3818) -> Option<HashMap<String, String>> {
3819    use crate::parser::automata::FIELD_MARKER_AUTOMATON;
3820
3821    let mut option_map: HashMap<String, String> = HashMap::new();
3822
3823    let mut ended_with_blank: bool = false;
3824
3825    while let Some(line) = src_lines.get(line_cursor.relative_offset()) {
3826        if line.trim().is_empty() {
3827            ended_with_blank = true;
3828            break;
3829        } // End of option list
3830
3831        if let Some(captures) = FIELD_MARKER_AUTOMATON.captures(line) {
3832            let line_indent = captures.get(1).unwrap().as_str().chars().count();
3833            if line_indent != body_indent {
3834                break;
3835            } // Option lists need to be aligned
3836            let option_key = captures.get(2).unwrap().as_str().trim();
3837
3838            let option_val_indent = captures.get(0).unwrap().as_str().chars().count();
3839            let option_val = match line.char_indices().nth(option_val_indent) {
3840                Some((index, _)) => line[index..].trim(),
3841                None => "",
3842            };
3843
3844            if let Some(val) = option_map.insert(option_key.to_string(), option_val.to_string())
3845            {
3846                // eprintln!("Duplicate directive option on line {}\n", line_cursor.sum_total())
3847            }
3848        } else {
3849            ended_with_blank = false;
3850            break; // Found a line not conforming to field list item syntax
3851        }
3852        line_cursor.increment_by(1);
3853    }
3854
3855    if option_map.is_empty() {
3856        None
3857    } else {
3858        if ended_with_blank {
3859            line_cursor.increment_by(1)
3860        }
3861        Some(option_map)
3862    }
3863}
3864
3865/// Parses the given `&str` for the directive key,
3866/// difficulty and maximum points.
3867/// Empty strings are returned for every missing part.
3868fn aplus_key_difficulty_and_max_points(
3869    arg_str: &str,
3870    line_cursor: &mut LineCursor,
3871) -> (String, String, String) {
3872    use regex::Regex;
3873
3874    lazy_static::lazy_static! {
3875        static ref EXERCISE_ARGS_RE: Regex = Regex::new(r"^(?P<key>[a-zA-Z0-9]+)?[ ]*(?P<difficulty>[A-Z])?(?P<max_points>[0-9]+)?").unwrap();
3876    }
3877
3878    if let Some(captures) = EXERCISE_ARGS_RE.captures(arg_str) {
3879        let key = if let Some(key) = captures.name("key") {
3880            String::from(key.as_str())
3881        } else {
3882            String::new()
3883        };
3884        let difficulty = if let Some(difficulty) = captures.name("difficulty") {
3885            String::from(difficulty.as_str())
3886        } else {
3887            String::new()
3888        };
3889        let max_points = if let Some(points) = captures.name("max_points") {
3890            String::from(points.as_str())
3891        } else {
3892            String::new()
3893        };
3894
3895        (key, difficulty, max_points)
3896    } else {
3897        // No allocations for strings with zero size
3898        eprintln!("Either no arguments or invalid argument format for questionnaire preceding line {}...", line_cursor.sum_total());
3899        (String::new(), String::new(), String::new())
3900    }
3901}