1use serde::{Deserialize, Serialize};
29
30use crate::content::Block;
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct Abstract {
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub id: Option<String>,
43
44 pub children: Vec<Block>,
46
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub keywords: Vec<String>,
50
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub sections: Vec<AbstractSection>,
54}
55
56impl Abstract {
57 #[must_use]
59 pub fn new(children: Vec<Block>) -> Self {
60 Self {
61 id: None,
62 children,
63 keywords: Vec::new(),
64 sections: Vec::new(),
65 }
66 }
67
68 #[must_use]
70 pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
71 self.keywords = keywords;
72 self
73 }
74
75 #[must_use]
77 pub fn with_section(mut self, section: AbstractSection) -> Self {
78 self.sections.push(section);
79 self
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct AbstractSection {
87 pub section_type: AbstractSectionType,
89
90 pub children: Vec<Block>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "lowercase")]
97pub enum AbstractSectionType {
98 Background,
100 Objectives,
102 Methods,
104 Results,
106 Conclusions,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct Theorem {
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub id: Option<String>,
121
122 pub variant: TheoremVariant,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub label: Option<String>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub number: Option<String>,
132
133 pub children: Vec<Block>,
135
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub attribution: Option<String>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub citation: Option<String>,
143
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub uses: Option<Vec<String>>,
147
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub restate: Option<bool>,
151}
152
153impl Theorem {
154 #[must_use]
156 pub fn new(variant: TheoremVariant, children: Vec<Block>) -> Self {
157 Self {
158 id: None,
159 variant,
160 label: None,
161 number: None,
162 children,
163 attribution: None,
164 citation: None,
165 uses: None,
166 restate: None,
167 }
168 }
169
170 #[must_use]
172 pub fn with_label(mut self, label: impl Into<String>) -> Self {
173 self.label = Some(label.into());
174 self
175 }
176
177 #[must_use]
179 pub fn with_number(mut self, number: impl Into<String>) -> Self {
180 self.number = Some(number.into());
181 self
182 }
183
184 #[must_use]
186 pub fn with_id(mut self, id: impl Into<String>) -> Self {
187 self.id = Some(id.into());
188 self
189 }
190
191 #[must_use]
193 pub fn with_attribution(mut self, attribution: impl Into<String>) -> Self {
194 self.attribution = Some(attribution.into());
195 self
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
201#[serde(rename_all = "lowercase")]
202pub enum TheoremVariant {
203 Theorem,
205 Lemma,
207 Proposition,
209 Corollary,
211 Definition,
213 Conjecture,
215 Remark,
217 Example,
219 Axiom,
221 Claim,
223 Fact,
225 Assumption,
227}
228
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct Proof {
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub id: Option<String>,
240
241 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub theorem_ref: Option<String>,
244
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub method: Option<ProofMethod>,
248
249 pub children: Vec<Block>,
251
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub qed_symbol: Option<String>,
255}
256
257impl Proof {
258 #[must_use]
260 pub fn new(children: Vec<Block>) -> Self {
261 Self {
262 id: None,
263 theorem_ref: None,
264 method: None,
265 children,
266 qed_symbol: None,
267 }
268 }
269
270 #[must_use]
272 pub fn of_theorem(mut self, theorem_id: impl Into<String>) -> Self {
273 self.theorem_ref = Some(theorem_id.into());
274 self
275 }
276
277 #[must_use]
279 pub fn with_method(mut self, method: ProofMethod) -> Self {
280 self.method = Some(method);
281 self
282 }
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "lowercase")]
288pub enum ProofMethod {
289 Direct,
291 Contradiction,
293 Contrapositive,
295 Induction,
297 StrongInduction,
299 Cases,
301 Constructive,
303 Existence,
305 Uniqueness,
307 Sketch,
309 StructuralInduction,
311 Counting,
313 Probabilistic,
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct Exercise {
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub id: Option<String>,
328
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub number: Option<String>,
332
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub difficulty: Option<Difficulty>,
336
337 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub points: Option<u32>,
340
341 pub children: Vec<Block>,
343
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub parts: Vec<ExercisePart>,
347
348 #[serde(default, skip_serializing_if = "Vec::is_empty")]
350 pub hints: Vec<Block>,
351
352 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub solution: Option<Solution>,
355}
356
357impl Exercise {
358 #[must_use]
360 pub fn new(children: Vec<Block>) -> Self {
361 Self {
362 id: None,
363 number: None,
364 difficulty: None,
365 points: None,
366 children,
367 parts: Vec::new(),
368 hints: Vec::new(),
369 solution: None,
370 }
371 }
372
373 #[must_use]
375 pub fn with_number(mut self, number: impl Into<String>) -> Self {
376 self.number = Some(number.into());
377 self
378 }
379
380 #[must_use]
382 pub fn with_difficulty(mut self, difficulty: Difficulty) -> Self {
383 self.difficulty = Some(difficulty);
384 self
385 }
386
387 #[must_use]
389 pub fn with_points(mut self, points: u32) -> Self {
390 self.points = Some(points);
391 self
392 }
393
394 #[must_use]
396 pub fn with_part(mut self, part: ExercisePart) -> Self {
397 self.parts.push(part);
398 self
399 }
400
401 #[must_use]
403 pub fn with_hint(mut self, hint: Vec<Block>) -> Self {
404 self.hints.extend(hint);
405 self
406 }
407
408 #[must_use]
410 pub fn with_solution(mut self, solution: Solution) -> Self {
411 self.solution = Some(solution);
412 self
413 }
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "lowercase")]
419pub enum Difficulty {
420 Easy,
422 Medium,
424 Hard,
426 Challenge,
428}
429
430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct ExercisePart {
434 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub label: Option<String>,
437
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub points: Option<u32>,
441
442 pub children: Vec<Block>,
444}
445
446#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct Solution {
450 #[serde(default)]
452 pub hidden: bool,
453
454 pub children: Vec<Block>,
456}
457
458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct ExerciseSet {
466 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub id: Option<String>,
469
470 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub title: Option<String>,
473
474 #[serde(default, skip_serializing_if = "Vec::is_empty")]
476 pub context: Vec<Block>,
477
478 pub exercises: Vec<Exercise>,
480
481 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub total_points: Option<u32>,
484}
485
486impl ExerciseSet {
487 #[must_use]
489 pub fn new(exercises: Vec<Exercise>) -> Self {
490 Self {
491 id: None,
492 title: None,
493 context: Vec::new(),
494 exercises,
495 total_points: None,
496 }
497 }
498
499 #[must_use]
501 pub fn with_title(mut self, title: impl Into<String>) -> Self {
502 self.title = Some(title.into());
503 self
504 }
505
506 #[must_use]
508 pub fn with_context(mut self, context: Vec<Block>) -> Self {
509 self.context = context;
510 self
511 }
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct EquationGroup {
522 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub id: Option<String>,
525
526 pub environment: EquationEnvironment,
528
529 pub lines: Vec<EquationLine>,
531}
532
533impl EquationGroup {
534 #[must_use]
536 pub fn new(environment: EquationEnvironment, lines: Vec<EquationLine>) -> Self {
537 Self {
538 id: None,
539 environment,
540 lines,
541 }
542 }
543
544 #[must_use]
546 pub fn with_id(mut self, id: impl Into<String>) -> Self {
547 self.id = Some(id.into());
548 self
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
554#[serde(rename_all = "lowercase")]
555pub enum EquationEnvironment {
556 Align,
558 Gather,
560 Multline,
562 Split,
564 Cases,
566 Alignat,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
572#[serde(rename_all = "camelCase")]
573pub struct EquationLine {
574 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub id: Option<String>,
577
578 pub value: String,
580
581 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub number: Option<String>,
584
585 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub tag: Option<String>,
588}
589
590impl EquationLine {
591 #[must_use]
593 pub fn new(value: impl Into<String>) -> Self {
594 Self {
595 id: None,
596 value: value.into(),
597 number: None,
598 tag: None,
599 }
600 }
601
602 #[must_use]
604 pub fn with_number(mut self, number: impl Into<String>) -> Self {
605 self.number = Some(number.into());
606 self
607 }
608
609 #[must_use]
611 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
612 self.tag = Some(tag.into());
613 self
614 }
615
616 #[must_use]
618 pub fn with_id(mut self, id: impl Into<String>) -> Self {
619 self.id = Some(id.into());
620 self
621 }
622}
623
624fn default_true() -> bool {
629 true
630}
631
632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635pub struct Algorithm {
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub id: Option<String>,
639
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub name: Option<String>,
643
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub number: Option<String>,
647
648 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub caption: Option<String>,
651
652 #[serde(default, skip_serializing_if = "Vec::is_empty")]
654 pub inputs: Vec<AlgorithmParam>,
655
656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
658 pub outputs: Vec<AlgorithmParam>,
659
660 pub body: Vec<AlgorithmLine>,
662
663 #[serde(default = "default_true")]
665 pub line_numbers: bool,
666
667 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub start_line: Option<u32>,
670}
671
672impl Algorithm {
673 #[must_use]
675 pub fn new(body: Vec<AlgorithmLine>) -> Self {
676 Self {
677 id: None,
678 name: None,
679 number: None,
680 caption: None,
681 inputs: Vec::new(),
682 outputs: Vec::new(),
683 body,
684 line_numbers: true,
685 start_line: None,
686 }
687 }
688
689 #[must_use]
691 pub fn with_name(mut self, name: impl Into<String>) -> Self {
692 self.name = Some(name.into());
693 self
694 }
695
696 #[must_use]
698 pub fn with_input(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
699 self.inputs.push(AlgorithmParam {
700 name: name.into(),
701 description: description.into(),
702 });
703 self
704 }
705
706 #[must_use]
708 pub fn with_output(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
709 self.outputs.push(AlgorithmParam {
710 name: name.into(),
711 description: description.into(),
712 });
713 self
714 }
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
719pub struct AlgorithmParam {
720 pub name: String,
722 pub description: String,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728#[serde(rename_all = "camelCase")]
729pub struct AlgorithmLine {
730 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub line_number: Option<u32>,
733
734 #[serde(default)]
736 pub indent: u8,
737
738 pub line_type: AlgorithmLineType,
740
741 pub content: String,
743
744 #[serde(default, skip_serializing_if = "Option::is_none")]
746 pub comment: Option<String>,
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
751#[serde(rename_all = "lowercase")]
752pub enum AlgorithmLineType {
753 Statement,
755 If,
757 ElseIf,
759 Else,
761 EndIf,
763 For,
765 EndFor,
767 While,
769 EndWhile,
771 Function,
773 EndFunction,
775 Return,
777 Comment,
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "camelCase")]
806pub struct EquationRef {
807 pub target: String,
809
810 #[serde(default, skip_serializing_if = "Option::is_none")]
812 pub format: Option<String>,
813}
814
815impl EquationRef {
816 #[must_use]
818 pub fn new(target: impl Into<String>) -> Self {
819 Self {
820 target: target.into(),
821 format: None,
822 }
823 }
824
825 #[must_use]
829 pub fn with_format(mut self, format: impl Into<String>) -> Self {
830 self.format = Some(format.into());
831 self
832 }
833
834 #[must_use]
836 pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
837 let mut attrs = serde_json::json!({
838 "target": self.target
839 });
840 if let Some(ref fmt) = self.format {
841 attrs["format"] = serde_json::Value::String(fmt.clone());
842 }
843 crate::content::ExtensionMark::new("academic", "equation-ref").with_attributes(attrs)
844 }
845}
846
847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
867#[serde(rename_all = "camelCase")]
868pub struct AlgorithmRef {
869 pub target: String,
871
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub line: Option<String>,
875
876 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub format: Option<String>,
879}
880
881impl AlgorithmRef {
882 #[must_use]
884 pub fn new(target: impl Into<String>) -> Self {
885 Self {
886 target: target.into(),
887 line: None,
888 format: None,
889 }
890 }
891
892 #[must_use]
894 pub fn with_line(mut self, line: impl Into<String>) -> Self {
895 self.line = Some(line.into());
896 self
897 }
898
899 #[must_use]
903 pub fn with_format(mut self, format: impl Into<String>) -> Self {
904 self.format = Some(format.into());
905 self
906 }
907
908 #[must_use]
910 pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
911 let mut attrs = serde_json::json!({
912 "target": self.target
913 });
914 if let Some(ref line) = self.line {
915 attrs["line"] = serde_json::Value::String(line.clone());
916 }
917 if let Some(ref fmt) = self.format {
918 attrs["format"] = serde_json::Value::String(fmt.clone());
919 }
920 crate::content::ExtensionMark::new("academic", "algorithm-ref").with_attributes(attrs)
921 }
922}
923
924#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
942#[serde(rename_all = "camelCase")]
943pub struct TheoremRef {
944 pub target: String,
946
947 #[serde(default, skip_serializing_if = "Option::is_none")]
949 pub format: Option<String>,
950}
951
952impl TheoremRef {
953 #[must_use]
955 pub fn new(target: impl Into<String>) -> Self {
956 Self {
957 target: target.into(),
958 format: None,
959 }
960 }
961
962 #[must_use]
966 pub fn with_format(mut self, format: impl Into<String>) -> Self {
967 self.format = Some(format.into());
968 self
969 }
970
971 #[must_use]
973 pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
974 let mut attrs = serde_json::json!({
975 "target": self.target
976 });
977 if let Some(ref fmt) = self.format {
978 attrs["format"] = serde_json::Value::String(fmt.clone());
979 }
980 crate::content::ExtensionMark::new("academic", "theorem-ref").with_attributes(attrs)
981 }
982}
983
984#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
990#[serde(rename_all = "camelCase")]
991pub struct NumberingConfig {
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub equations: Option<NumberingStyle>,
995
996 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub theorems: Option<NumberingStyle>,
999
1000 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub algorithms: Option<NumberingStyle>,
1003
1004 #[serde(default, skip_serializing_if = "Option::is_none")]
1006 pub figures: Option<NumberingStyle>,
1007
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub tables: Option<NumberingStyle>,
1011}
1012
1013#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1015#[serde(rename_all = "camelCase")]
1016pub struct NumberingStyle {
1017 pub format: String,
1019
1020 #[serde(default)]
1022 pub reset_per_chapter: bool,
1023
1024 #[serde(default = "default_start")]
1026 pub start: u32,
1027}
1028
1029fn default_start() -> u32 {
1030 1
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036
1037 #[test]
1038 fn test_theorem_new() {
1039 let thm = Theorem::new(TheoremVariant::Theorem, vec![]);
1040 assert_eq!(thm.variant, TheoremVariant::Theorem);
1041 assert!(thm.label.is_none());
1042 assert!(thm.number.is_none());
1043 }
1044
1045 #[test]
1046 fn test_theorem_builder() {
1047 let thm = Theorem::new(TheoremVariant::Lemma, vec![])
1048 .with_label("Pumping Lemma")
1049 .with_number("4.2")
1050 .with_id("pumping")
1051 .with_attribution("Bar-Hillel et al.");
1052
1053 assert_eq!(thm.variant, TheoremVariant::Lemma);
1054 assert_eq!(thm.label, Some("Pumping Lemma".to_string()));
1055 assert_eq!(thm.number, Some("4.2".to_string()));
1056 assert_eq!(thm.id, Some("pumping".to_string()));
1057 assert_eq!(thm.attribution, Some("Bar-Hillel et al.".to_string()));
1058 }
1059
1060 #[test]
1061 fn test_theorem_variant_display() {
1062 assert_eq!(TheoremVariant::Theorem.to_string(), "Theorem");
1063 assert_eq!(TheoremVariant::Lemma.to_string(), "Lemma");
1064 assert_eq!(TheoremVariant::Corollary.to_string(), "Corollary");
1065 }
1066
1067 #[test]
1068 fn test_theorem_serialization() {
1069 let thm = Theorem::new(TheoremVariant::Definition, vec![])
1070 .with_label("Continuity")
1071 .with_number("2.1");
1072
1073 let json = serde_json::to_string(&thm).unwrap();
1074 assert!(json.contains("\"variant\":\"definition\""));
1075 assert!(json.contains("\"label\":\"Continuity\""));
1076 assert!(json.contains("\"number\":\"2.1\""));
1077 }
1078
1079 #[test]
1080 fn test_proof_new() {
1081 let proof = Proof::new(vec![]);
1082 assert!(proof.theorem_ref.is_none());
1083 assert!(proof.method.is_none());
1084 }
1085
1086 #[test]
1087 fn test_proof_builder() {
1088 let proof = Proof::new(vec![])
1089 .of_theorem("thm-pythagoras")
1090 .with_method(ProofMethod::Direct);
1091
1092 assert_eq!(proof.theorem_ref, Some("thm-pythagoras".to_string()));
1093 assert_eq!(proof.method, Some(ProofMethod::Direct));
1094 }
1095
1096 #[test]
1097 fn test_exercise_new() {
1098 let ex = Exercise::new(vec![]);
1099 assert!(ex.number.is_none());
1100 assert!(ex.difficulty.is_none());
1101 }
1102
1103 #[test]
1104 fn test_exercise_builder() {
1105 let ex = Exercise::new(vec![])
1106 .with_number("3.5")
1107 .with_difficulty(Difficulty::Hard)
1108 .with_points(10);
1109
1110 assert_eq!(ex.number, Some("3.5".to_string()));
1111 assert_eq!(ex.difficulty, Some(Difficulty::Hard));
1112 assert_eq!(ex.points, Some(10));
1113 }
1114
1115 #[test]
1116 fn test_algorithm_new() {
1117 let alg = Algorithm::new(vec![]);
1118 assert!(alg.name.is_none());
1119 assert!(alg.line_numbers);
1120 }
1121
1122 #[test]
1123 fn test_algorithm_builder() {
1124 let alg = Algorithm::new(vec![])
1125 .with_name("QuickSort")
1126 .with_input("A", "array to sort")
1127 .with_output("A", "sorted array");
1128
1129 assert_eq!(alg.name, Some("QuickSort".to_string()));
1130 assert_eq!(alg.inputs.len(), 1);
1131 assert_eq!(alg.inputs[0].name, "A");
1132 assert_eq!(alg.outputs.len(), 1);
1133 }
1134
1135 #[test]
1136 fn test_equation_group() {
1137 let line = EquationLine::new("E = mc^2")
1138 .with_id("eq1")
1139 .with_number("(1)");
1140 let group = EquationGroup::new(EquationEnvironment::Align, vec![line]);
1141
1142 assert_eq!(group.environment, EquationEnvironment::Align);
1143 assert_eq!(group.lines.len(), 1);
1144 assert_eq!(group.lines[0].value, "E = mc^2");
1145 assert_eq!(group.lines[0].id, Some("eq1".to_string()));
1146 assert_eq!(group.lines[0].number, Some("(1)".to_string()));
1147 }
1148
1149 #[test]
1150 fn test_equation_line_with_tag() {
1151 let line = EquationLine::new("a^2 + b^2 = c^2").with_tag("*");
1152 assert_eq!(line.tag, Some("*".to_string()));
1153 assert!(line.number.is_none());
1154 }
1155
1156 #[test]
1157 fn test_equation_line_serde_roundtrip() {
1158 let line = EquationLine::new("f(x) = ax + b")
1159 .with_id("eq-fx")
1160 .with_number("2.1")
1161 .with_tag("linear");
1162 let json = serde_json::to_string(&line).unwrap();
1163 assert!(json.contains("\"value\":\"f(x) = ax + b\""));
1164 assert!(json.contains("\"number\":\"2.1\""));
1165 assert!(json.contains("\"tag\":\"linear\""));
1166
1167 let parsed: EquationLine = serde_json::from_str(&json).unwrap();
1168 assert_eq!(parsed.value, "f(x) = ax + b");
1169 assert_eq!(parsed.number, Some("2.1".to_string()));
1170 assert_eq!(parsed.tag, Some("linear".to_string()));
1171 }
1172
1173 #[test]
1174 fn test_equation_line_without_tag_defaults_to_none() {
1175 let json = r#"{"value": "x + y"}"#;
1176 let line: EquationLine = serde_json::from_str(json).unwrap();
1177 assert!(line.tag.is_none());
1178 assert!(line.number.is_none());
1179 assert!(line.id.is_none());
1180 }
1181
1182 #[test]
1183 fn test_equation_group_with_lines_serde() {
1184 let group = EquationGroup::new(
1185 EquationEnvironment::Gather,
1186 vec![
1187 EquationLine::new("a = b").with_number("1"),
1188 EquationLine::new("c = d").with_number("2"),
1189 ],
1190 )
1191 .with_id("eq-group-1");
1192
1193 let json = serde_json::to_string(&group).unwrap();
1194 assert!(json.contains("\"lines\""));
1195 assert!(json.contains("\"environment\":\"gather\""));
1196
1197 let parsed: EquationGroup = serde_json::from_str(&json).unwrap();
1198 assert_eq!(parsed.lines.len(), 2);
1199 assert_eq!(parsed.id, Some("eq-group-1".to_string()));
1200 }
1201
1202 #[test]
1203 fn test_alignat_environment_serialization() {
1204 let group = EquationGroup::new(
1205 EquationEnvironment::Alignat,
1206 vec![EquationLine::new("x &= y &= z")],
1207 );
1208 let json = serde_json::to_string(&group).unwrap();
1209 assert!(json.contains("\"environment\":\"alignat\""));
1210
1211 let parsed: EquationGroup = serde_json::from_str(&json).unwrap();
1212 assert_eq!(parsed.environment, EquationEnvironment::Alignat);
1213 }
1214
1215 #[test]
1216 fn test_abstract_new() {
1217 let abs = Abstract::new(vec![])
1218 .with_keywords(vec!["AI".to_string(), "Machine Learning".to_string()]);
1219
1220 assert_eq!(abs.keywords.len(), 2);
1221 assert!(abs.sections.is_empty());
1222 }
1223
1224 #[test]
1225 fn test_equation_ref() {
1226 let eq_ref = EquationRef::new("#eq-pythagoras");
1227 assert_eq!(eq_ref.target, "#eq-pythagoras");
1228 assert!(eq_ref.format.is_none());
1229
1230 let eq_ref_fmt = eq_ref.with_format("Equation ({number})");
1231 assert_eq!(eq_ref_fmt.format, Some("Equation ({number})".to_string()));
1232 }
1233
1234 #[test]
1235 fn test_equation_ref_to_mark() {
1236 let eq_ref = EquationRef::new("#eq-1").with_format("({number})");
1237 let mark = eq_ref.to_extension_mark();
1238
1239 assert_eq!(mark.namespace, "academic");
1240 assert_eq!(mark.mark_type, "equation-ref");
1241 assert_eq!(mark.get_string_attribute("target"), Some("#eq-1"));
1242 assert_eq!(mark.get_string_attribute("format"), Some("({number})"));
1243 }
1244
1245 #[test]
1246 fn test_algorithm_ref() {
1247 let alg_ref = AlgorithmRef::new("#alg-quicksort");
1248 assert_eq!(alg_ref.target, "#alg-quicksort");
1249 assert!(alg_ref.line.is_none());
1250 assert!(alg_ref.format.is_none());
1251 }
1252
1253 #[test]
1254 fn test_algorithm_ref_with_line() {
1255 let alg_ref = AlgorithmRef::new("#alg-bisection")
1256 .with_line("loop")
1257 .with_format("line {line}");
1258
1259 assert_eq!(alg_ref.target, "#alg-bisection");
1260 assert_eq!(alg_ref.line, Some("loop".to_string()));
1261 assert_eq!(alg_ref.format, Some("line {line}".to_string()));
1262 }
1263
1264 #[test]
1265 fn test_algorithm_ref_to_mark() {
1266 let alg_ref = AlgorithmRef::new("#alg-1")
1267 .with_line("start")
1268 .with_format("Algorithm {number}, line {line}");
1269 let mark = alg_ref.to_extension_mark();
1270
1271 assert_eq!(mark.namespace, "academic");
1272 assert_eq!(mark.mark_type, "algorithm-ref");
1273 assert_eq!(mark.get_string_attribute("target"), Some("#alg-1"));
1274 assert_eq!(mark.get_string_attribute("line"), Some("start"));
1275 assert_eq!(
1276 mark.get_string_attribute("format"),
1277 Some("Algorithm {number}, line {line}")
1278 );
1279 }
1280
1281 #[test]
1282 fn test_theorem_ref() {
1283 let thm_ref = TheoremRef::new("#thm-pythagoras");
1284 assert_eq!(thm_ref.target, "#thm-pythagoras");
1285 assert!(thm_ref.format.is_none());
1286 }
1287
1288 #[test]
1289 fn test_theorem_ref_to_mark() {
1290 let thm_ref = TheoremRef::new("#thm-1").with_format("{variant} {number}");
1291 let mark = thm_ref.to_extension_mark();
1292
1293 assert_eq!(mark.namespace, "academic");
1294 assert_eq!(mark.mark_type, "theorem-ref");
1295 assert_eq!(mark.get_string_attribute("target"), Some("#thm-1"));
1296 assert_eq!(
1297 mark.get_string_attribute("format"),
1298 Some("{variant} {number}")
1299 );
1300 }
1301
1302 #[test]
1303 fn test_equation_ref_serialization() {
1304 let eq_ref = EquationRef::new("#eq-fx").with_format("({number})");
1305 let json = serde_json::to_string(&eq_ref).unwrap();
1306 assert!(json.contains("\"target\":\"#eq-fx\""));
1307 assert!(json.contains("\"format\":\"({number})\""));
1308
1309 let parsed: EquationRef = serde_json::from_str(&json).unwrap();
1311 assert_eq!(parsed.target, "#eq-fx");
1312 assert_eq!(parsed.format, Some("({number})".to_string()));
1313 }
1314
1315 #[test]
1316 fn test_algorithm_ref_serialization() {
1317 let alg_ref = AlgorithmRef::new("#alg-sort")
1318 .with_line("pivot")
1319 .with_format("line {line}");
1320 let json = serde_json::to_string(&alg_ref).unwrap();
1321 assert!(json.contains("\"target\":\"#alg-sort\""));
1322 assert!(json.contains("\"line\":\"pivot\""));
1323 assert!(json.contains("\"format\":\"line {line}\""));
1324 }
1325
1326 #[test]
1327 fn test_theorem_uses_and_restate_roundtrip() {
1328 let thm = Theorem {
1329 id: Some("thm-2".to_string()),
1330 variant: TheoremVariant::Corollary,
1331 label: None,
1332 number: None,
1333 children: vec![],
1334 attribution: None,
1335 citation: None,
1336 uses: Some(vec!["#thm-1".to_string(), "#lemma-1".to_string()]),
1337 restate: Some(true),
1338 };
1339 let json = serde_json::to_string(&thm).unwrap();
1340 assert!(json.contains("\"uses\":[\"#thm-1\",\"#lemma-1\"]"));
1341 assert!(json.contains("\"restate\":true"));
1342
1343 let parsed: Theorem = serde_json::from_str(&json).unwrap();
1344 assert_eq!(
1345 parsed.uses,
1346 Some(vec!["#thm-1".to_string(), "#lemma-1".to_string()])
1347 );
1348 assert_eq!(parsed.restate, Some(true));
1349 }
1350
1351 #[test]
1352 fn test_theorem_without_new_fields_defaults_to_none() {
1353 let json = r#"{
1354 "variant": "theorem",
1355 "children": []
1356 }"#;
1357 let thm: Theorem = serde_json::from_str(json).unwrap();
1358 assert!(thm.uses.is_none());
1359 assert!(thm.restate.is_none());
1360 }
1361
1362 #[test]
1363 fn test_new_proof_method_variants() {
1364 let methods = [
1365 (ProofMethod::StructuralInduction, "structuralinduction"),
1366 (ProofMethod::Counting, "counting"),
1367 (ProofMethod::Probabilistic, "probabilistic"),
1368 ];
1369 for (method, expected_str) in methods {
1370 let json = serde_json::to_string(&method).unwrap();
1371 assert_eq!(json, format!("\"{expected_str}\""));
1372 let parsed: ProofMethod = serde_json::from_str(&json).unwrap();
1373 assert_eq!(parsed, method);
1374 }
1375 }
1376
1377 #[test]
1378 fn test_algorithm_start_line_roundtrip() {
1379 let alg = Algorithm {
1380 id: None,
1381 name: Some("BFS".to_string()),
1382 number: None,
1383 caption: None,
1384 inputs: Vec::new(),
1385 outputs: Vec::new(),
1386 body: vec![],
1387 line_numbers: true,
1388 start_line: Some(10),
1389 };
1390 let json = serde_json::to_string(&alg).unwrap();
1391 assert!(json.contains("\"startLine\":10"));
1392
1393 let parsed: Algorithm = serde_json::from_str(&json).unwrap();
1394 assert_eq!(parsed.start_line, Some(10));
1395 }
1396
1397 #[test]
1398 fn test_algorithm_without_start_line_defaults_to_none() {
1399 let json = r#"{
1400 "body": [],
1401 "lineNumbers": true
1402 }"#;
1403 let alg: Algorithm = serde_json::from_str(json).unwrap();
1404 assert!(alg.start_line.is_none());
1405 }
1406}