flams_system/backend/
docfile.rs1use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
2
3use eyre::{eyre, Context};
4use flams_ontology::narration::{
5 documents::{Document, UncheckedDocument},
6 problems::{Quiz, QuizElement, QuizProblem},
7 DocumentElement, NarrationTrait,
8};
9use flams_utils::{impossible, vecmap::VecSet};
10use smallvec::SmallVec;
11
12use super::Backend;
13
14pub struct PreDocFile;
26
27impl PreDocFile {
28 pub(crate) fn read_from_file(path: &Path) -> Option<UncheckedDocument> {
29 macro_rules! err {
30 ($e:expr) => {
31 match $e {
32 Ok(e) => e,
33 Err(e) => {
34 tracing::error!("Error loading {}: {e}", path.display());
35 return None;
36 }
37 }
38 };
39 }
40 let file = err!(File::open(path));
41 let file = BufReader::new(file);
42 Some(err!(bincode::serde::decode_from_reader(
44 file,
45 bincode::config::standard()
46 )))
47 }
51}
52
53pub trait QuizExtension {
54 fn as_quiz(&self, backend: &impl Backend) -> eyre::Result<Quiz>;
56}
57impl QuizExtension for Document {
58 #[allow(clippy::redundant_else)]
59 #[allow(clippy::too_many_lines)]
60 fn as_quiz(&self, backend: &impl Backend) -> eyre::Result<Quiz> {
61 let mut css = VecSet::default();
62 let mut elements = Vec::new();
63 let mut solutions = HashMap::default();
64 let mut answer_classes: HashMap<_, Vec<_>> = HashMap::default();
65 let mut in_problem = false;
66
67 let mut stack: SmallVec<_, 2> = SmallVec::new();
68 let mut curr = self.children().iter();
69
70 macro_rules! push {
71 ($c:expr;$e:expr) => {
72 stack.push((
73 std::mem::replace(&mut curr, $c),
74 std::mem::take(&mut elements),
75 $e,
76 ))
77 };
78 }
79 macro_rules! pop {
80 () => {
81 if let Some((c, mut e, s)) = stack.pop() {
82 curr = c;
83 std::mem::swap(&mut elements, &mut e);
84 match s {
85 Some(either::Either::Left(s)) => elements.push(QuizElement::Section {
86 title: s,
87 elements: e,
88 }),
89 Some(either::Either::Right(b)) => {
90 in_problem = b;
91 elements.extend(e.into_iter());
92 }
93 _ => elements.extend(e.into_iter()),
94 }
95 continue;
96 } else {
97 break;
98 }
99 };
100 }
101
102 loop {
103 let Some(e) = curr.next() else { pop!() };
104 match e {
105 DocumentElement::DocumentReference { target, .. } => {
106 let uri = target.id();
108 let Some(d) = backend.get_document(&uri) else {
109 return Err(eyre!("Missing document {uri}"));
110 };
111 let ret = d.as_quiz(backend)?;
112 for c in ret.css {
123 css.insert(c);
124 }
125 elements.extend(ret.elements);
126 for (u, s) in ret.solutions {
127 solutions.insert(u, s);
128 }
129 }
130 DocumentElement::Section(sect) => {
131 if let Some(title) = sect.title {
132 let Some((c, s)) = backend.get_html_fragment(self.uri(), title) else {
133 return Err(eyre!("Missing FTML fragment for {}", sect.uri));
134 };
135 for c in c {
136 css.insert(c);
137 }
138 push!(sect.children().iter();Some(either::Either::Left(s)));
139 } else {
140 push!(sect.children().iter();None);
141 }
142 }
143 DocumentElement::Paragraph(p) => {
144 let Some((c, html)) = backend.get_html_fragment(self.uri(), p.range) else {
145 return Err(eyre!("Missing FTML fragment for {}", p.uri));
146 };
147 for c in c {
148 css.insert(c);
149 }
150 elements.push(QuizElement::Paragraph { html });
151 }
152 DocumentElement::Problem(e) if in_problem => {
153 let solution = backend
154 .get_reference(&e.solutions)
155 .wrap_err_with(|| format!("Missing solutions for {}", e.uri))?;
156 let Some(solution) = solution.to_jstring() else {
157 return Err(eyre!("Invalid solutions for {}", e.uri));
158 };
159 solutions.insert(e.uri.clone(), solution);
160 }
161 DocumentElement::Problem(e) => {
162 let Some((c, html)) = backend.get_html_fragment(self.uri(), e.range) else {
163 return Err(eyre!("Missing FTML fragment for {}", e.uri));
164 };
165 for c in c {
166 css.insert(c);
167 }
168 let solution = backend
169 .get_reference(&e.solutions)
170 .wrap_err_with(|| format!("Missing solutions for {}", e.uri))?;
171 let title_html = if let Some(ttl) = e.title {
172 let Some(t) = backend.get_html_fragment(self.uri(), ttl) else {
173 return Err(eyre!("Missing FTML fragment for title of {}", e.uri));
174 };
175 Some(t.1)
176 } else {
177 None
178 };
179 let Some(solution) = solution.to_jstring() else {
180 return Err(eyre!("Invalid solutions for {}", e.uri));
181 };
182 for note in &e.gnotes {
183 let gnote = backend
184 .get_reference(note)
185 .wrap_err_with(|| format!("Missing gnote for {}", e.uri))?;
186 answer_classes
187 .entry(e.uri.clone())
188 .or_default()
189 .extend(gnote.answer_classes);
190 }
191 solutions.insert(e.uri.clone(), solution);
192 elements.push(QuizElement::Problem(QuizProblem {
193 html, title_html,
195 uri: e.uri.clone(),
196 preconditions: e.preconditions.to_vec(),
197 objectives: e.objectives.to_vec(),
198 total_points: e.points,
199 }));
200 push!(e.children().iter();Some(either::Either::Right(in_problem)));
201 in_problem = true;
202 }
203 e => {
204 let c = e.children();
205 if !c.is_empty() {
206 push!(c.iter();None);
207 }
208 }
209 }
210 }
211 if elements.len() == 1 && matches!(elements.first(), Some(QuizElement::Section { .. })) {
212 let Some(QuizElement::Section { elements: es, .. }) = elements.pop() else {
213 impossible!()
214 };
215 elements = es;
216 }
217 Ok(Quiz {
218 title: self.title().map(ToString::to_string),
219 answer_classes,
220 elements,
221 css: css.0,
222 solutions,
223 })
224 }
225}