1use flams_utils::CSS;
2use smallvec::SmallVec;
3use std::{collections::HashMap, fmt::Display, str::FromStr};
4
5use crate::{
6 uris::{DocumentElementURI, Name, SymbolURI},
7 Checked, CheckingState, DocumentRange,
8};
9
10use super::{DocumentElement, LazyDocRef, NarrationTrait};
11
12#[cfg(feature = "wasm")]
13use wasm_bindgen::prelude::wasm_bindgen;
14
15#[derive(Debug)]
16pub struct Problem<State: CheckingState> {
17 pub sub_problem: bool,
18 pub uri: DocumentElementURI,
19 pub range: DocumentRange,
20 pub autogradable: bool,
21 pub points: Option<f32>,
22 pub solutions: LazyDocRef<Solutions>, pub gnotes: State::Seq<LazyDocRef<GradingNote>>,
24 pub hints: State::Seq<DocumentRange>,
25 pub notes: State::Seq<LazyDocRef<Box<str>>>,
26 pub title: Option<DocumentRange>,
27 pub children: State::Seq<DocumentElement<State>>,
28 pub styles: Box<[Name]>,
29 pub preconditions: State::Seq<(CognitiveDimension, SymbolURI)>,
30 pub objectives: State::Seq<(CognitiveDimension, SymbolURI)>,
31}
32
33#[cfg(not(feature = "wasm"))]
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Solutions(Box<[SolutionData]>);
37
38#[cfg(feature = "wasm")]
39#[allow(clippy::unsafe_derive_deserialize)]
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
41#[wasm_bindgen]
42pub struct Solutions(Box<[SolutionData]>);
43
44#[cfg_attr(feature = "wasm", wasm_bindgen)]
45impl Solutions {
46 #[cfg(feature = "serde")]
47 #[must_use]
48 pub fn from_jstring(s: &str) -> Option<Self> {
49 use flams_utils::Hexable;
50 Self::from_hex(s).ok()
51 }
52 #[cfg(feature = "serde")]
53 #[must_use]
54 pub fn to_jstring(&self) -> Option<String> {
55 use flams_utils::Hexable;
56 self.as_hex().ok()
57 }
58
59 #[inline]
60 pub fn from_solutions(solutions: Box<[SolutionData]>) -> Self {
61 Self(solutions)
62 }
63
64 #[inline]
65 pub fn to_solutions(&self) -> Box<[SolutionData]> {
66 self.0.clone()
67 }
68
69 #[must_use]
70 #[inline]
71 pub fn check_response(&self, response: &ProblemResponse) -> Option<ProblemFeedback> {
72 self.check(response)
73 }
74
75 #[must_use]
76 #[inline]
77 pub fn default_feedback(&self) -> ProblemFeedback {
78 self.default()
79 }
80}
81
82impl crate::Resourcable for Solutions {}
83impl Solutions {
84 #[must_use]
85 pub fn default(&self) -> ProblemFeedback {
86 let mut solutions = SmallVec::new();
87 let mut data = SmallVec::new();
88 for sol in self.0.iter() {
89 match sol {
90 SolutionData::Solution { html, .. } => solutions.push(html.clone()),
91 SolutionData::ChoiceBlock(ChoiceBlock {
92 multiple: false,
93 choices,
94 ..
95 }) => data.push(CheckedResult::SingleChoice {
96 selected: None,
97 choices: choices
98 .iter()
99 .enumerate()
100 .map(|(_, c)| BlockFeedback {
101 is_correct: c.correct,
102 verdict_str: c.verdict.to_string(),
103 feedback: c.feedback.to_string(),
104 })
105 .collect(),
106 }),
107 SolutionData::ChoiceBlock(ChoiceBlock { choices, .. }) => {
108 data.push(CheckedResult::MultipleChoice {
109 selected: choices.iter().map(|_| false).collect(),
110 choices: choices
111 .iter()
112 .enumerate()
113 .map(|(_, c)| BlockFeedback {
114 is_correct: c.correct,
115 verdict_str: c.verdict.to_string(),
116 feedback: c.feedback.to_string(),
117 })
118 .collect(),
119 })
120 }
121 SolutionData::FillInSol(f) => {
122 let mut options = SmallVec::new();
123 for o in f.opts.iter() {
124 match o {
125 FillInSolOption::Exact {
126 value,
127 verdict,
128 feedback,
129 } => options.push(FillinFeedback {
130 is_correct: *verdict,
131 feedback: feedback.to_string(),
132 kind: FillinFeedbackKind::Exact(value.to_string()),
133 }),
134 FillInSolOption::NumericalRange {
135 from,
136 to,
137 verdict,
138 feedback,
139 } => options.push(FillinFeedback {
140 is_correct: *verdict,
141 feedback: feedback.to_string(),
142 kind: FillinFeedbackKind::NumRange {
143 from: *from,
144 to: *to,
145 },
146 }),
147 FillInSolOption::Regex {
148 regex,
149 verdict,
150 feedback,
151 } => options.push(FillinFeedback {
152 is_correct: *verdict,
153 feedback: feedback.to_string(),
154 kind: FillinFeedbackKind::Regex(regex.as_str().to_string()),
155 }),
156 }
157 }
158 data.push(CheckedResult::FillinSol {
159 matching: None,
160 options,
161 text: String::new(),
162 });
163 }
164 }
165 }
166
167 ProblemFeedback {
168 correct: false,
169 solutions,
170 data,
171 score_fraction: 0.0,
172 }
173 }
174
175 #[must_use]
176 #[allow(clippy::too_many_lines)]
177 #[allow(clippy::cast_precision_loss)]
178 pub fn check(&self, response: &ProblemResponse) -> Option<ProblemFeedback> {
179 fn next_sol<'a>(
181 solutions: &mut SmallVec<Box<str>, 1>,
182 datas: &mut impl Iterator<Item = &'a SolutionData>,
183 ) -> Option<&'a SolutionData> {
184 loop {
185 match datas.next() {
186 None => return None,
187 Some(SolutionData::Solution { html, .. }) => solutions.push(html.clone()),
188 Some(c) => return Some(c),
189 }
190 }
191 }
192 let mut correct = true;
193 let mut pts: f32 = 0.0;
194 let mut total: f32 = 0.0;
195 let mut solutions = SmallVec::new();
196 let mut data = SmallVec::new();
197 let mut datas = self.0.iter();
198
199 for response in &response.responses {
200 total += 1.0;
201 let sol = next_sol(&mut solutions, &mut datas)?;
202 match (response, sol) {
203 (
204 ProblemResponseType::SingleChoice { value: selected },
205 SolutionData::ChoiceBlock(ChoiceBlock {
206 multiple: false,
207 choices,
208 ..
209 }),
210 ) => data.push(CheckedResult::SingleChoice {
211 selected: *selected,
212 choices: choices
213 .iter()
214 .enumerate()
215 .map(
216 |(
217 i,
218 Choice {
219 correct: cr,
220 verdict,
221 feedback,
222 },
223 )| {
224 if selected.is_some_and(|j| j as usize == i) {
225 correct = correct && *cr;
226 if *cr {
227 pts += 1.0;
228 }
229 }
230 BlockFeedback {
231 is_correct: *cr,
232 verdict_str: verdict.to_string(),
233 feedback: feedback.to_string(),
234 }
235 },
236 )
237 .collect(),
238 }),
239 (
240 ProblemResponseType::MultipleChoice { value: selected },
241 SolutionData::ChoiceBlock(ChoiceBlock {
242 multiple: true,
243 choices,
244 ..
245 }),
246 ) => {
247 if selected.len() != choices.len() {
248 return None;
249 }
250 let mut corrects = 0;
251 let mut falses = 0;
252 data.push(CheckedResult::MultipleChoice {
253 selected: selected.clone(),
254 choices: choices
255 .iter()
256 .enumerate()
257 .map(
258 |(
259 i,
260 Choice {
261 correct: cr,
262 verdict,
263 feedback,
264 },
265 )| {
266 if *cr == selected[i] {
267 corrects += 1;
268 } else {
269 falses += 1;
270 }
271 correct = correct && (selected[i] == *cr);
272 BlockFeedback {
273 is_correct: *cr,
274 verdict_str: verdict.to_string(),
275 feedback: feedback.to_string(),
276 }
277 },
278 )
279 .collect(),
280 });
281 if selected.iter().any(|b| *b) {
282 pts += ((corrects as f32 - falses as f32) / choices.len() as f32).max(0.0);
283 }
284 }
285 (ProblemResponseType::Fillinsol { value: s }, SolutionData::FillInSol(f)) => {
286 let mut fill_correct = None;
287 let mut matching = None;
288 let mut options = SmallVec::new();
289 for (i, o) in f.opts.iter().enumerate() {
290 match o {
291 FillInSolOption::Exact {
292 value: string,
293 verdict,
294 feedback,
295 } => {
296 if fill_correct.is_none() && &**string == s.as_str() {
297 if *verdict {
298 pts += 1.0;
299 }
300 fill_correct = Some(*verdict);
301 matching = Some(i);
302 }
303 options.push(FillinFeedback {
304 is_correct: *verdict,
305 feedback: feedback.to_string(),
306 kind: FillinFeedbackKind::Exact(string.to_string()),
307 });
308 }
309 FillInSolOption::NumericalRange {
310 from,
311 to,
312 verdict,
313 feedback,
314 } => {
315 if fill_correct.is_none() {
316 let num = if s.contains('.') {
317 s.parse::<f32>().ok()
318 } else {
319 s.parse::<i32>().ok().map(|i| i as f32)
320 };
321 if let Some(f) = num {
322 if !from.is_some_and(|v| f < v)
323 && !to.is_some_and(|v| f > v)
324 {
325 if *verdict {
326 pts += 1.0;
327 }
328 fill_correct = Some(*verdict);
329 matching = Some(i);
330 }
331 }
332 }
333 options.push(FillinFeedback {
334 is_correct: *verdict,
335 feedback: feedback.to_string(),
336 kind: FillinFeedbackKind::NumRange {
337 from: *from,
338 to: *to,
339 },
340 });
341 }
342 FillInSolOption::Regex {
343 regex,
344 verdict,
345 feedback,
346 } => {
347 if fill_correct.is_none() && regex.is_match(s) {
348 if *verdict {
349 pts += 1.0;
350 }
351 fill_correct = Some(*verdict);
352 matching = Some(i);
353 }
354 options.push(FillinFeedback {
355 is_correct: *verdict,
356 feedback: feedback.to_string(),
357 kind: FillinFeedbackKind::Regex(regex.as_str().to_string()),
358 });
359 }
360 }
361 }
362 correct = correct && fill_correct.unwrap_or_default();
363 data.push(CheckedResult::FillinSol {
364 matching,
365 options,
366 text: s.to_string(),
367 });
368 }
369 _ => return None,
370 }
371 }
372
373 if next_sol(&mut solutions, &mut datas).is_some() {
374 return None;
375 }
376
377 Some(ProblemFeedback {
378 correct,
379 solutions,
380 data,
381 score_fraction: pts / total,
382 })
383 }
384}
385
386crate::serde_impl! {
387 struct Problem[
388 sub_problem,uri,range,autogradable,points,solutions,gnotes,
389 hints,notes,title,children,styles,preconditions,
390 objectives
391 ]
392}
393
394#[derive(Debug, Clone)]
395#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
396#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
397#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
398pub enum SolutionData {
399 Solution {
400 html: Box<str>,
401 answer_class: Option<Box<str>>,
402 },
403 ChoiceBlock(ChoiceBlock),
404 FillInSol(FillInSol),
405}
406
407#[derive(Debug, Clone)]
408#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
409#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
410#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
411pub struct ChoiceBlock {
412 pub multiple: bool,
413 pub inline: bool,
414 pub range: DocumentRange,
415 pub styles: Box<[Box<str>]>,
416 pub choices: Vec<Choice>,
417}
418
419#[derive(Debug, Clone)]
420#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
421#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
422#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
423pub struct Choice {
424 pub correct: bool,
425 pub verdict: Box<str>,
426 pub feedback: Box<str>,
427}
428
429#[derive(Debug, Clone)]
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
432#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
433pub struct FillInSol {
434 pub width: Option<f32>,
435 pub opts: Vec<FillInSolOption>,
436}
437
438#[derive(Debug, Clone)]
439#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
440#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
441#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
442pub enum FillInSolOption {
443 Exact {
444 value: Box<str>,
445 verdict: bool,
446 feedback: Box<str>,
447 },
448 NumericalRange {
449 from: Option<f32>,
450 to: Option<f32>,
451 verdict: bool,
452 feedback: Box<str>,
453 },
454 Regex {
455 regex: flams_utils::regex::Regex,
456 verdict: bool,
457 feedback: Box<str>,
458 },
459}
460impl FillInSolOption {
461 #[allow(clippy::cast_precision_loss)]
462 #[must_use]
463 pub fn from_values(kind: &str, value: &str, verdict: bool) -> Option<Self> {
464 match kind {
465 "exact" => Some(Self::Exact {
466 value: value.to_string().into(),
467 verdict,
468 feedback: String::new().into(),
469 }),
470 "numrange" => {
471 let (s, neg) = value
472 .strip_prefix('-')
473 .map_or((value, false), |s| (s, true));
474 let (from, to) = if let Some((from, to)) = s.split_once('-') {
475 (from, to)
476 } else {
477 ("", s)
478 };
479 let from = if from.contains('.') {
480 Some(f32::from_str(from).ok()?)
481 } else if from.is_empty() {
482 None
483 } else {
484 Some(i128::from_str(from).ok()? as _)
485 };
486 let from = if neg { from.map(|f| -f) } else { from };
487 let to = if to.contains('.') {
488 Some(f32::from_str(to).ok()?)
489 } else if to.is_empty() {
490 None
491 } else {
492 Some(i128::from_str(to).ok()? as _)
493 };
494 Some(Self::NumericalRange {
495 from,
496 to,
497 verdict,
498 feedback: String::new().into(),
499 })
500 }
501 "regex" => Some(Self::Regex {
502 regex: flams_utils::regex::Regex::new(
503 value, )
505 .ok()?,
506 verdict,
507 feedback: String::new().into(),
508 }),
509 _ => None,
510 }
511 }
512}
513
514#[derive(Debug, Clone)]
515#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
516pub struct GradingNote {
517 pub html: Box<str>,
518 pub answer_classes: Vec<AnswerClass>,
519}
520impl crate::Resourcable for GradingNote {}
521
522#[cfg(not(feature = "wasm"))]
523#[derive(Debug, Clone)]
524#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
525pub struct ProblemFeedback {
526 pub correct: bool,
527 pub solutions: SmallVec<Box<str>, 1>,
528 pub data: SmallVec<CheckedResult, 4>,
529 pub score_fraction: f32,
530}
531
532#[cfg(feature = "wasm")]
533#[allow(clippy::unsafe_derive_deserialize)]
534#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
535#[wasm_bindgen]
536pub struct ProblemFeedback {
537 pub correct: bool,
538 #[wasm_bindgen(skip)]
539 pub solutions: SmallVec<Box<str>, 1>,
540 #[wasm_bindgen(skip)]
542 pub data: SmallVec<CheckedResult, 4>,
543 pub score_fraction: f32,
544}
545
546#[cfg(feature = "wasm")]
547#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
548#[tsify(from_wasm_abi, into_wasm_abi)]
549pub struct ProblemFeedbackJson {
550 pub correct: bool,
551 #[cfg_attr(feature = "wasm", tsify(type = "string[]"))]
552 pub solutions: SmallVec<Box<str>, 1>,
553 #[cfg_attr(feature = "wasm", tsify(type = "CheckedResult[]"))]
554 pub data: SmallVec<CheckedResult, 4>,
555 pub score_fraction: f32,
556}
557
558#[cfg(feature = "wasm")]
559#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
560impl ProblemFeedback {
561 #[must_use]
562 pub fn from_jstring(s: &str) -> Option<Self> {
563 use flams_utils::Hexable;
564 Self::from_hex(s).ok()
565 }
566
567 #[cfg(feature = "serde")]
568 #[must_use]
569 pub fn to_jstring(&self) -> Option<String> {
570 use flams_utils::Hexable;
571 self.as_hex().ok()
572 }
573
574 #[inline]
575 pub fn from_json(
576 ProblemFeedbackJson {
577 correct,
578 solutions,
579 data,
580 score_fraction,
581 }: ProblemFeedbackJson,
582 ) -> Self {
583 Self {
584 correct,
585 solutions,
586 data,
587 score_fraction,
588 }
589 }
590
591 #[inline]
592 pub fn to_json(&self) -> ProblemFeedbackJson {
593 let Self {
594 correct,
595 solutions,
596 data,
597 score_fraction,
598 } = self.clone();
599 ProblemFeedbackJson {
600 correct,
601 solutions,
602 data,
603 score_fraction,
604 }
605 }
606}
607
608#[derive(Debug, Clone)]
609#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
610#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
611#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
612pub struct BlockFeedback {
613 pub is_correct: bool,
614 pub verdict_str: String,
615 pub feedback: String,
616}
617
618#[derive(Debug, Clone)]
619#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
620#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
621#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
622pub struct FillinFeedback {
623 pub is_correct: bool,
624 pub feedback: String,
625 pub kind: FillinFeedbackKind,
626}
627
628#[derive(Debug, Clone)]
629#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
630#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
631#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
632pub enum FillinFeedbackKind {
633 Exact(String),
634 NumRange { from: Option<f32>, to: Option<f32> },
635 Regex(String),
636}
637
638#[derive(Debug, Clone)]
639#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
640#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
641#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
642#[cfg_attr(feature = "serde", serde(tag = "type"))]
643pub enum CheckedResult {
644 SingleChoice {
645 selected: Option<u16>,
646 #[cfg_attr(feature = "wasm", tsify(type = "BlockFeedback[]"))]
647 choices: SmallVec<BlockFeedback, 4>,
648 },
649 MultipleChoice {
650 #[cfg_attr(feature = "wasm", tsify(type = "boolean[]"))]
651 selected: SmallVec<bool, 8>,
652 #[cfg_attr(feature = "wasm", tsify(type = "BlockFeedback[]"))]
653 choices: SmallVec<BlockFeedback, 4>,
654 },
655 FillinSol {
656 matching: Option<usize>,
657 text: String,
658 #[cfg_attr(feature = "wasm", tsify(type = "FillinFeedback[]"))]
659 options: SmallVec<FillinFeedback, 4>,
660 },
661}
662
663#[derive(Debug, Clone)]
664#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
665#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
666#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
667pub struct ProblemResponse {
669 pub uri: DocumentElementURI,
670 #[cfg_attr(feature = "wasm", tsify(type = "ProblemResponseType[]"))]
671 pub responses: SmallVec<ProblemResponseType, 4>,
672}
673
674#[derive(Debug, Clone)]
675#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
676#[cfg_attr(feature = "serde", serde(tag = "type"))]
677#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
678#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
679pub enum ProblemResponseType {
682 MultipleChoice {
683 #[cfg_attr(feature = "wasm", tsify(type = "boolean[]"))]
684 value: SmallVec<bool, 8>,
685 },
686 SingleChoice {
687 value: Option<u16>,
688 },
689 Fillinsol {
690 #[serde(rename = "value")]
691 value: String,
692 },
693}
694
695#[derive(Debug, Clone)]
696#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
697#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
698#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
699pub struct AnswerClass {
700 pub id: Box<str>,
701 pub feedback: Box<str>,
702 pub kind: AnswerKind,
703}
704
705#[derive(Debug, Clone, Copy)]
706#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
707#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
708#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
709pub enum AnswerKind {
710 Class(f32),
711 Trait(f32),
712}
713impl FromStr for AnswerKind {
714 type Err = ();
715 fn from_str(s: &str) -> Result<Self, Self::Err> {
716 #[allow(clippy::cast_precision_loss)]
717 fn num(s: &str) -> Result<f32, ()> {
718 if s.contains('.') {
719 s.parse().map_err(|_| ())
720 } else {
721 let i: Result<i32, ()> = s.parse().map_err(|_| ());
722 i.map(|i| i as f32)
723 }
724 }
725 let s = s.trim();
726 s.strip_prefix('+').map_or_else(
727 || {
728 s.strip_prefix('-').map_or_else(
729 || num(s).map(AnswerKind::Class),
730 |s| num(s).map(|f| Self::Trait(-f)),
731 )
732 },
733 |s| num(s).map(AnswerKind::Trait),
734 )
735 }
736}
737
738impl NarrationTrait for Problem<Checked> {
739 #[inline]
740 fn children(&self) -> &[DocumentElement<Checked>] {
741 &self.children
742 }
743 #[inline]
744 fn from_element(e: &DocumentElement<Checked>) -> Option<&Self>
745 where
746 Self: Sized,
747 {
748 if let DocumentElement::Problem(e) = e {
749 Some(e)
750 } else {
751 None
752 }
753 }
754}
755
756#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
757#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
758#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
759#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
760pub enum CognitiveDimension {
761 Remember,
762 Understand,
763 Apply,
764 Analyze,
765 Evaluate,
766 Create,
767}
768impl CognitiveDimension {
769 #[cfg(feature = "rdf")]
770 #[must_use]
771 pub const fn to_iri(&self) -> crate::rdf::NamedNodeRef {
772 use crate::rdf::ontologies::ulo2;
773 use CognitiveDimension::*;
774 match self {
775 Remember => ulo2::REMEMBER,
776 Understand => ulo2::UNDERSTAND,
777 Apply => ulo2::APPLY,
778 Analyze => ulo2::ANALYZE,
779 Evaluate => ulo2::EVALUATE,
780 Create => ulo2::CREATE,
781 }
782 }
783}
784impl Display for CognitiveDimension {
785 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
786 use CognitiveDimension::*;
787 write!(
788 f,
789 "{}",
790 match self {
791 Remember => "remember",
792 Understand => "understand",
793 Apply => "apply",
794 Analyze => "analyze",
795 Evaluate => "evaluate",
796 Create => "create",
797 }
798 )
799 }
800}
801impl FromStr for CognitiveDimension {
802 type Err = ();
803 fn from_str(s: &str) -> Result<Self, Self::Err> {
804 use CognitiveDimension::*;
805 Ok(match s {
806 "remember" => Remember,
807 "understand" => Understand,
808 "apply" => Apply,
809 "analyze" | "analyse" => Analyze,
810 "evaluate" => Evaluate,
811 "create" => Create,
812 _ => return Err(()),
813 })
814 }
815}
816
817#[derive(Debug, Clone)]
818#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
819#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
820#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
821pub struct Quiz {
822 pub css: Vec<CSS>,
823 pub title: Option<String>,
824 pub elements: Vec<QuizElement>,
825 pub solutions: HashMap<DocumentElementURI, String>,
826 pub answer_classes: HashMap<DocumentElementURI, Vec<AnswerClass>>,
827}
828
829#[derive(Debug, Clone)]
830#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
831#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
832#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
833pub enum QuizElement {
834 Section {
835 title: String,
836 elements: Vec<QuizElement>,
837 },
838 Problem(QuizProblem),
839 Paragraph {
840 html: String,
841 },
842}
843
844#[derive(Debug, Clone)]
845#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
846#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
847#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
848pub struct QuizProblem {
849 pub html: String,
850 pub title_html: Option<String>,
851 pub uri: DocumentElementURI,
852 pub total_points: Option<f32>,
854 pub preconditions: Vec<(CognitiveDimension, SymbolURI)>,
856 pub objectives: Vec<(CognitiveDimension, SymbolURI)>,
857}