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, Copy, PartialEq, Eq, Serialize, Deserialize)]
991#[serde(rename_all = "camelCase")]
992pub enum ResetTrigger {
993 Heading1,
995 Heading2,
997 Heading3,
999 Heading4,
1001 Heading5,
1003 Heading6,
1005 None,
1007}
1008
1009#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1011pub enum NumberingStylePattern {
1012 #[serde(rename = "number")]
1014 Number,
1015 #[serde(rename = "chapter.number")]
1017 ChapterNumber,
1018 #[serde(rename = "section.number")]
1020 SectionNumber,
1021 #[serde(rename = "chapter.section.number")]
1023 ChapterSectionNumber,
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1028#[serde(rename_all = "camelCase")]
1029pub struct NumberingConfig {
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1032 pub equations: Option<NumberingStyle>,
1033
1034 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub theorems: Option<NumberingStyle>,
1037
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1040 pub algorithms: Option<NumberingStyle>,
1041
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1044 pub figures: Option<NumberingStyle>,
1045
1046 #[serde(default, skip_serializing_if = "Option::is_none")]
1048 pub tables: Option<NumberingStyle>,
1049}
1050
1051#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1053#[serde(rename_all = "camelCase")]
1054pub struct NumberingStyle {
1055 #[serde(default, skip_serializing_if = "Option::is_none", alias = "format")]
1057 pub style: Option<NumberingStylePattern>,
1058
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1061 pub reset_on: Option<ResetTrigger>,
1062
1063 #[serde(default = "default_start")]
1065 pub start: u32,
1066}
1067
1068fn default_start() -> u32 {
1069 1
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1077 fn test_theorem_new() {
1078 let thm = Theorem::new(TheoremVariant::Theorem, vec![]);
1079 assert_eq!(thm.variant, TheoremVariant::Theorem);
1080 assert!(thm.label.is_none());
1081 assert!(thm.number.is_none());
1082 }
1083
1084 #[test]
1085 fn test_theorem_builder() {
1086 let thm = Theorem::new(TheoremVariant::Lemma, vec![])
1087 .with_label("Pumping Lemma")
1088 .with_number("4.2")
1089 .with_id("pumping")
1090 .with_attribution("Bar-Hillel et al.");
1091
1092 assert_eq!(thm.variant, TheoremVariant::Lemma);
1093 assert_eq!(thm.label, Some("Pumping Lemma".to_string()));
1094 assert_eq!(thm.number, Some("4.2".to_string()));
1095 assert_eq!(thm.id, Some("pumping".to_string()));
1096 assert_eq!(thm.attribution, Some("Bar-Hillel et al.".to_string()));
1097 }
1098
1099 #[test]
1100 fn test_theorem_variant_display() {
1101 assert_eq!(TheoremVariant::Theorem.to_string(), "Theorem");
1102 assert_eq!(TheoremVariant::Lemma.to_string(), "Lemma");
1103 assert_eq!(TheoremVariant::Corollary.to_string(), "Corollary");
1104 }
1105
1106 #[test]
1107 fn test_theorem_serialization() {
1108 let thm = Theorem::new(TheoremVariant::Definition, vec![])
1109 .with_label("Continuity")
1110 .with_number("2.1");
1111
1112 let json = serde_json::to_string(&thm).unwrap();
1113 assert!(json.contains("\"variant\":\"definition\""));
1114 assert!(json.contains("\"label\":\"Continuity\""));
1115 assert!(json.contains("\"number\":\"2.1\""));
1116 }
1117
1118 #[test]
1119 fn test_proof_new() {
1120 let proof = Proof::new(vec![]);
1121 assert!(proof.theorem_ref.is_none());
1122 assert!(proof.method.is_none());
1123 }
1124
1125 #[test]
1126 fn test_proof_builder() {
1127 let proof = Proof::new(vec![])
1128 .of_theorem("thm-pythagoras")
1129 .with_method(ProofMethod::Direct);
1130
1131 assert_eq!(proof.theorem_ref, Some("thm-pythagoras".to_string()));
1132 assert_eq!(proof.method, Some(ProofMethod::Direct));
1133 }
1134
1135 #[test]
1136 fn test_exercise_new() {
1137 let ex = Exercise::new(vec![]);
1138 assert!(ex.number.is_none());
1139 assert!(ex.difficulty.is_none());
1140 }
1141
1142 #[test]
1143 fn test_exercise_builder() {
1144 let ex = Exercise::new(vec![])
1145 .with_number("3.5")
1146 .with_difficulty(Difficulty::Hard)
1147 .with_points(10);
1148
1149 assert_eq!(ex.number, Some("3.5".to_string()));
1150 assert_eq!(ex.difficulty, Some(Difficulty::Hard));
1151 assert_eq!(ex.points, Some(10));
1152 }
1153
1154 #[test]
1155 fn test_algorithm_new() {
1156 let alg = Algorithm::new(vec![]);
1157 assert!(alg.name.is_none());
1158 assert!(alg.line_numbers);
1159 }
1160
1161 #[test]
1162 fn test_algorithm_builder() {
1163 let alg = Algorithm::new(vec![])
1164 .with_name("QuickSort")
1165 .with_input("A", "array to sort")
1166 .with_output("A", "sorted array");
1167
1168 assert_eq!(alg.name, Some("QuickSort".to_string()));
1169 assert_eq!(alg.inputs.len(), 1);
1170 assert_eq!(alg.inputs[0].name, "A");
1171 assert_eq!(alg.outputs.len(), 1);
1172 }
1173
1174 #[test]
1175 fn test_equation_group() {
1176 let line = EquationLine::new("E = mc^2")
1177 .with_id("eq1")
1178 .with_number("(1)");
1179 let group = EquationGroup::new(EquationEnvironment::Align, vec![line]);
1180
1181 assert_eq!(group.environment, EquationEnvironment::Align);
1182 assert_eq!(group.lines.len(), 1);
1183 assert_eq!(group.lines[0].value, "E = mc^2");
1184 assert_eq!(group.lines[0].id, Some("eq1".to_string()));
1185 assert_eq!(group.lines[0].number, Some("(1)".to_string()));
1186 }
1187
1188 #[test]
1189 fn test_equation_line_with_tag() {
1190 let line = EquationLine::new("a^2 + b^2 = c^2").with_tag("*");
1191 assert_eq!(line.tag, Some("*".to_string()));
1192 assert!(line.number.is_none());
1193 }
1194
1195 #[test]
1196 fn test_equation_line_serde_roundtrip() {
1197 let line = EquationLine::new("f(x) = ax + b")
1198 .with_id("eq-fx")
1199 .with_number("2.1")
1200 .with_tag("linear");
1201 let json = serde_json::to_string(&line).unwrap();
1202 assert!(json.contains("\"value\":\"f(x) = ax + b\""));
1203 assert!(json.contains("\"number\":\"2.1\""));
1204 assert!(json.contains("\"tag\":\"linear\""));
1205
1206 let parsed: EquationLine = serde_json::from_str(&json).unwrap();
1207 assert_eq!(parsed.value, "f(x) = ax + b");
1208 assert_eq!(parsed.number, Some("2.1".to_string()));
1209 assert_eq!(parsed.tag, Some("linear".to_string()));
1210 }
1211
1212 #[test]
1213 fn test_equation_line_without_tag_defaults_to_none() {
1214 let json = r#"{"value": "x + y"}"#;
1215 let line: EquationLine = serde_json::from_str(json).unwrap();
1216 assert!(line.tag.is_none());
1217 assert!(line.number.is_none());
1218 assert!(line.id.is_none());
1219 }
1220
1221 #[test]
1222 fn test_equation_group_with_lines_serde() {
1223 let group = EquationGroup::new(
1224 EquationEnvironment::Gather,
1225 vec![
1226 EquationLine::new("a = b").with_number("1"),
1227 EquationLine::new("c = d").with_number("2"),
1228 ],
1229 )
1230 .with_id("eq-group-1");
1231
1232 let json = serde_json::to_string(&group).unwrap();
1233 assert!(json.contains("\"lines\""));
1234 assert!(json.contains("\"environment\":\"gather\""));
1235
1236 let parsed: EquationGroup = serde_json::from_str(&json).unwrap();
1237 assert_eq!(parsed.lines.len(), 2);
1238 assert_eq!(parsed.id, Some("eq-group-1".to_string()));
1239 }
1240
1241 #[test]
1242 fn test_alignat_environment_serialization() {
1243 let group = EquationGroup::new(
1244 EquationEnvironment::Alignat,
1245 vec![EquationLine::new("x &= y &= z")],
1246 );
1247 let json = serde_json::to_string(&group).unwrap();
1248 assert!(json.contains("\"environment\":\"alignat\""));
1249
1250 let parsed: EquationGroup = serde_json::from_str(&json).unwrap();
1251 assert_eq!(parsed.environment, EquationEnvironment::Alignat);
1252 }
1253
1254 #[test]
1255 fn test_abstract_new() {
1256 let abs = Abstract::new(vec![])
1257 .with_keywords(vec!["AI".to_string(), "Machine Learning".to_string()]);
1258
1259 assert_eq!(abs.keywords.len(), 2);
1260 assert!(abs.sections.is_empty());
1261 }
1262
1263 #[test]
1264 fn test_equation_ref() {
1265 let eq_ref = EquationRef::new("#eq-pythagoras");
1266 assert_eq!(eq_ref.target, "#eq-pythagoras");
1267 assert!(eq_ref.format.is_none());
1268
1269 let eq_ref_fmt = eq_ref.with_format("Equation ({number})");
1270 assert_eq!(eq_ref_fmt.format, Some("Equation ({number})".to_string()));
1271 }
1272
1273 #[test]
1274 fn test_equation_ref_to_mark() {
1275 let eq_ref = EquationRef::new("#eq-1").with_format("({number})");
1276 let mark = eq_ref.to_extension_mark();
1277
1278 assert_eq!(mark.namespace, "academic");
1279 assert_eq!(mark.mark_type, "equation-ref");
1280 assert_eq!(mark.get_string_attribute("target"), Some("#eq-1"));
1281 assert_eq!(mark.get_string_attribute("format"), Some("({number})"));
1282 }
1283
1284 #[test]
1285 fn test_algorithm_ref() {
1286 let alg_ref = AlgorithmRef::new("#alg-quicksort");
1287 assert_eq!(alg_ref.target, "#alg-quicksort");
1288 assert!(alg_ref.line.is_none());
1289 assert!(alg_ref.format.is_none());
1290 }
1291
1292 #[test]
1293 fn test_algorithm_ref_with_line() {
1294 let alg_ref = AlgorithmRef::new("#alg-bisection")
1295 .with_line("loop")
1296 .with_format("line {line}");
1297
1298 assert_eq!(alg_ref.target, "#alg-bisection");
1299 assert_eq!(alg_ref.line, Some("loop".to_string()));
1300 assert_eq!(alg_ref.format, Some("line {line}".to_string()));
1301 }
1302
1303 #[test]
1304 fn test_algorithm_ref_to_mark() {
1305 let alg_ref = AlgorithmRef::new("#alg-1")
1306 .with_line("start")
1307 .with_format("Algorithm {number}, line {line}");
1308 let mark = alg_ref.to_extension_mark();
1309
1310 assert_eq!(mark.namespace, "academic");
1311 assert_eq!(mark.mark_type, "algorithm-ref");
1312 assert_eq!(mark.get_string_attribute("target"), Some("#alg-1"));
1313 assert_eq!(mark.get_string_attribute("line"), Some("start"));
1314 assert_eq!(
1315 mark.get_string_attribute("format"),
1316 Some("Algorithm {number}, line {line}")
1317 );
1318 }
1319
1320 #[test]
1321 fn test_theorem_ref() {
1322 let thm_ref = TheoremRef::new("#thm-pythagoras");
1323 assert_eq!(thm_ref.target, "#thm-pythagoras");
1324 assert!(thm_ref.format.is_none());
1325 }
1326
1327 #[test]
1328 fn test_theorem_ref_to_mark() {
1329 let thm_ref = TheoremRef::new("#thm-1").with_format("{variant} {number}");
1330 let mark = thm_ref.to_extension_mark();
1331
1332 assert_eq!(mark.namespace, "academic");
1333 assert_eq!(mark.mark_type, "theorem-ref");
1334 assert_eq!(mark.get_string_attribute("target"), Some("#thm-1"));
1335 assert_eq!(
1336 mark.get_string_attribute("format"),
1337 Some("{variant} {number}")
1338 );
1339 }
1340
1341 #[test]
1342 fn test_equation_ref_serialization() {
1343 let eq_ref = EquationRef::new("#eq-fx").with_format("({number})");
1344 let json = serde_json::to_string(&eq_ref).unwrap();
1345 assert!(json.contains("\"target\":\"#eq-fx\""));
1346 assert!(json.contains("\"format\":\"({number})\""));
1347
1348 let parsed: EquationRef = serde_json::from_str(&json).unwrap();
1350 assert_eq!(parsed.target, "#eq-fx");
1351 assert_eq!(parsed.format, Some("({number})".to_string()));
1352 }
1353
1354 #[test]
1355 fn test_algorithm_ref_serialization() {
1356 let alg_ref = AlgorithmRef::new("#alg-sort")
1357 .with_line("pivot")
1358 .with_format("line {line}");
1359 let json = serde_json::to_string(&alg_ref).unwrap();
1360 assert!(json.contains("\"target\":\"#alg-sort\""));
1361 assert!(json.contains("\"line\":\"pivot\""));
1362 assert!(json.contains("\"format\":\"line {line}\""));
1363 }
1364
1365 #[test]
1366 fn test_theorem_uses_and_restate_roundtrip() {
1367 let thm = Theorem {
1368 id: Some("thm-2".to_string()),
1369 variant: TheoremVariant::Corollary,
1370 label: None,
1371 number: None,
1372 children: vec![],
1373 attribution: None,
1374 citation: None,
1375 uses: Some(vec!["#thm-1".to_string(), "#lemma-1".to_string()]),
1376 restate: Some(true),
1377 };
1378 let json = serde_json::to_string(&thm).unwrap();
1379 assert!(json.contains("\"uses\":[\"#thm-1\",\"#lemma-1\"]"));
1380 assert!(json.contains("\"restate\":true"));
1381
1382 let parsed: Theorem = serde_json::from_str(&json).unwrap();
1383 assert_eq!(
1384 parsed.uses,
1385 Some(vec!["#thm-1".to_string(), "#lemma-1".to_string()])
1386 );
1387 assert_eq!(parsed.restate, Some(true));
1388 }
1389
1390 #[test]
1391 fn test_theorem_without_new_fields_defaults_to_none() {
1392 let json = r#"{
1393 "variant": "theorem",
1394 "children": []
1395 }"#;
1396 let thm: Theorem = serde_json::from_str(json).unwrap();
1397 assert!(thm.uses.is_none());
1398 assert!(thm.restate.is_none());
1399 }
1400
1401 #[test]
1402 fn test_new_proof_method_variants() {
1403 let methods = [
1404 (ProofMethod::StructuralInduction, "structuralinduction"),
1405 (ProofMethod::Counting, "counting"),
1406 (ProofMethod::Probabilistic, "probabilistic"),
1407 ];
1408 for (method, expected_str) in methods {
1409 let json = serde_json::to_string(&method).unwrap();
1410 assert_eq!(json, format!("\"{expected_str}\""));
1411 let parsed: ProofMethod = serde_json::from_str(&json).unwrap();
1412 assert_eq!(parsed, method);
1413 }
1414 }
1415
1416 #[test]
1417 fn test_algorithm_start_line_roundtrip() {
1418 let alg = Algorithm {
1419 id: None,
1420 name: Some("BFS".to_string()),
1421 number: None,
1422 caption: None,
1423 inputs: Vec::new(),
1424 outputs: Vec::new(),
1425 body: vec![],
1426 line_numbers: true,
1427 start_line: Some(10),
1428 };
1429 let json = serde_json::to_string(&alg).unwrap();
1430 assert!(json.contains("\"startLine\":10"));
1431
1432 let parsed: Algorithm = serde_json::from_str(&json).unwrap();
1433 assert_eq!(parsed.start_line, Some(10));
1434 }
1435
1436 #[test]
1437 fn test_algorithm_without_start_line_defaults_to_none() {
1438 let json = r#"{
1439 "body": [],
1440 "lineNumbers": true
1441 }"#;
1442 let alg: Algorithm = serde_json::from_str(json).unwrap();
1443 assert!(alg.start_line.is_none());
1444 }
1445}