Skip to main content

cdx_core/extensions/
academic.rs

1//! Academic extension for Codex documents.
2//!
3//! This extension provides specialized content types for academic and
4//! scientific documents including theorems, proofs, exercises, and algorithms.
5//!
6//! # Features
7//!
8//! - **Abstract**: Paper abstracts with keywords and structured sections
9//! - **Theorem**: Theorem-like blocks (theorem, lemma, proposition, etc.)
10//! - **Proof**: Proof blocks with method annotations
11//! - **Exercise**: Exercises with hints and solutions
12//! - **`ExerciseSet`**: Grouped exercises with shared context
13//! - **`EquationGroup`**: Multi-line equation environments
14//! - **Algorithm**: Pseudocode blocks with line numbering
15//!
16//! # Example
17//!
18//! ```json
19//! {
20//!   "type": "academic:theorem",
21//!   "variant": "theorem",
22//!   "label": "Pythagorean Theorem",
23//!   "number": "3.1",
24//!   "children": [...]
25//! }
26//! ```
27
28use serde::{Deserialize, Serialize};
29
30use crate::content::Block;
31
32// ============================================================================
33// Abstract
34// ============================================================================
35
36/// An academic abstract with optional keywords and structured sections.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct Abstract {
40    /// Optional unique identifier.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub id: Option<String>,
43
44    /// Abstract content blocks.
45    pub children: Vec<Block>,
46
47    /// Keywords for the paper.
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub keywords: Vec<String>,
50
51    /// Structured sections within the abstract (background, methods, results, conclusions).
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub sections: Vec<AbstractSection>,
54}
55
56impl Abstract {
57    /// Create a new abstract.
58    #[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    /// Add keywords.
69    #[must_use]
70    pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
71        self.keywords = keywords;
72        self
73    }
74
75    /// Add a section.
76    #[must_use]
77    pub fn with_section(mut self, section: AbstractSection) -> Self {
78        self.sections.push(section);
79        self
80    }
81}
82
83/// A structured section within an abstract.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct AbstractSection {
87    /// Section type.
88    pub section_type: AbstractSectionType,
89
90    /// Section content.
91    pub children: Vec<Block>,
92}
93
94/// Types of structured abstract sections.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "lowercase")]
97pub enum AbstractSectionType {
98    /// Background/context.
99    Background,
100    /// Research objectives.
101    Objectives,
102    /// Methods used.
103    Methods,
104    /// Results obtained.
105    Results,
106    /// Conclusions drawn.
107    Conclusions,
108}
109
110// ============================================================================
111// Theorem
112// ============================================================================
113
114/// A theorem-like block (theorem, lemma, proposition, etc.).
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct Theorem {
118    /// Optional unique identifier for cross-referencing.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub id: Option<String>,
121
122    /// Type of theorem-like statement.
123    pub variant: TheoremVariant,
124
125    /// Optional label/title for the theorem.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub label: Option<String>,
128
129    /// Theorem number (e.g., "3.1", "A.2").
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub number: Option<String>,
132
133    /// Statement content.
134    pub children: Vec<Block>,
135
136    /// Attribution (e.g., "Euclid", "Fermat").
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub attribution: Option<String>,
139
140    /// Citation reference.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub citation: Option<String>,
143
144    /// Content Anchor URIs of theorems this depends on.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub uses: Option<Vec<String>>,
147
148    /// Whether this restates an existing theorem.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub restate: Option<bool>,
151}
152
153impl Theorem {
154    /// Create a new theorem.
155    #[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    /// Set the label.
171    #[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    /// Set the number.
178    #[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    /// Set the ID.
185    #[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    /// Set attribution.
192    #[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/// Variant of theorem-like statement.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
201#[serde(rename_all = "lowercase")]
202pub enum TheoremVariant {
203    /// Main theorem.
204    Theorem,
205    /// Lemma (helper result).
206    Lemma,
207    /// Proposition.
208    Proposition,
209    /// Corollary (follows from theorem).
210    Corollary,
211    /// Definition.
212    Definition,
213    /// Conjecture (unproven).
214    Conjecture,
215    /// Remark.
216    Remark,
217    /// Example.
218    Example,
219    /// Axiom.
220    Axiom,
221    /// Claim.
222    Claim,
223    /// Fact.
224    Fact,
225    /// Assumption.
226    Assumption,
227}
228
229// ============================================================================
230// Proof
231// ============================================================================
232
233/// A proof block.
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct Proof {
237    /// Optional unique identifier.
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub id: Option<String>,
240
241    /// Reference to the theorem being proved.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub theorem_ref: Option<String>,
244
245    /// Proof method used.
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub method: Option<ProofMethod>,
248
249    /// Proof content.
250    pub children: Vec<Block>,
251
252    /// Custom QED symbol (defaults to standard square).
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub qed_symbol: Option<String>,
255}
256
257impl Proof {
258    /// Create a new proof.
259    #[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    /// Set the theorem reference.
271    #[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    /// Set the proof method.
278    #[must_use]
279    pub fn with_method(mut self, method: ProofMethod) -> Self {
280        self.method = Some(method);
281        self
282    }
283}
284
285/// Method of proof.
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "lowercase")]
288pub enum ProofMethod {
289    /// Direct proof.
290    Direct,
291    /// Proof by contradiction.
292    Contradiction,
293    /// Proof by contrapositive.
294    Contrapositive,
295    /// Proof by induction.
296    Induction,
297    /// Strong induction.
298    StrongInduction,
299    /// Proof by cases.
300    Cases,
301    /// Constructive proof.
302    Constructive,
303    /// Existence proof.
304    Existence,
305    /// Uniqueness proof.
306    Uniqueness,
307    /// Proof sketch.
308    Sketch,
309    /// Structural induction.
310    StructuralInduction,
311    /// Counting argument.
312    Counting,
313    /// Probabilistic argument.
314    Probabilistic,
315}
316
317// ============================================================================
318// Exercise
319// ============================================================================
320
321/// An exercise with optional hints and solutions.
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct Exercise {
325    /// Optional unique identifier.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub id: Option<String>,
328
329    /// Exercise number.
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub number: Option<String>,
332
333    /// Difficulty level.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub difficulty: Option<Difficulty>,
336
337    /// Points/marks for the exercise.
338    #[serde(default, skip_serializing_if = "Option::is_none")]
339    pub points: Option<u32>,
340
341    /// Exercise statement.
342    pub children: Vec<Block>,
343
344    /// Sub-parts of the exercise.
345    #[serde(default, skip_serializing_if = "Vec::is_empty")]
346    pub parts: Vec<ExercisePart>,
347
348    /// Hints.
349    #[serde(default, skip_serializing_if = "Vec::is_empty")]
350    pub hints: Vec<Block>,
351
352    /// Solution (may be hidden).
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub solution: Option<Solution>,
355}
356
357impl Exercise {
358    /// Create a new exercise.
359    #[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    /// Set the number.
374    #[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    /// Set difficulty.
381    #[must_use]
382    pub fn with_difficulty(mut self, difficulty: Difficulty) -> Self {
383        self.difficulty = Some(difficulty);
384        self
385    }
386
387    /// Set points.
388    #[must_use]
389    pub fn with_points(mut self, points: u32) -> Self {
390        self.points = Some(points);
391        self
392    }
393
394    /// Add a part.
395    #[must_use]
396    pub fn with_part(mut self, part: ExercisePart) -> Self {
397        self.parts.push(part);
398        self
399    }
400
401    /// Add a hint.
402    #[must_use]
403    pub fn with_hint(mut self, hint: Vec<Block>) -> Self {
404        self.hints.extend(hint);
405        self
406    }
407
408    /// Set the solution.
409    #[must_use]
410    pub fn with_solution(mut self, solution: Solution) -> Self {
411        self.solution = Some(solution);
412        self
413    }
414}
415
416/// Difficulty level for exercises.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "lowercase")]
419pub enum Difficulty {
420    /// Easy difficulty.
421    Easy,
422    /// Medium difficulty.
423    Medium,
424    /// Hard difficulty.
425    Hard,
426    /// Challenge problem.
427    Challenge,
428}
429
430/// A sub-part of an exercise.
431#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct ExercisePart {
434    /// Part label (a, b, c or i, ii, iii).
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub label: Option<String>,
437
438    /// Points for this part.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub points: Option<u32>,
441
442    /// Part content.
443    pub children: Vec<Block>,
444}
445
446/// A solution to an exercise.
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct Solution {
450    /// Whether the solution should be hidden.
451    #[serde(default)]
452    pub hidden: bool,
453
454    /// Solution content.
455    pub children: Vec<Block>,
456}
457
458// ============================================================================
459// ExerciseSet
460// ============================================================================
461
462/// A set of related exercises with shared context.
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct ExerciseSet {
466    /// Optional unique identifier.
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub id: Option<String>,
469
470    /// Title of the exercise set.
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub title: Option<String>,
473
474    /// Shared context/preamble for all exercises.
475    #[serde(default, skip_serializing_if = "Vec::is_empty")]
476    pub context: Vec<Block>,
477
478    /// Exercises in the set.
479    pub exercises: Vec<Exercise>,
480
481    /// Total points for the set.
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub total_points: Option<u32>,
484}
485
486impl ExerciseSet {
487    /// Create a new exercise set.
488    #[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    /// Set the title.
500    #[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    /// Set context.
507    #[must_use]
508    pub fn with_context(mut self, context: Vec<Block>) -> Self {
509        self.context = context;
510        self
511    }
512}
513
514// ============================================================================
515// EquationGroup
516// ============================================================================
517
518/// A group of related equations (align, gather, etc.).
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct EquationGroup {
522    /// Optional unique identifier.
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub id: Option<String>,
525
526    /// Environment type.
527    pub environment: EquationEnvironment,
528
529    /// Equation lines in the group.
530    pub lines: Vec<EquationLine>,
531}
532
533impl EquationGroup {
534    /// Create a new equation group.
535    #[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    /// Set the ID.
545    #[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/// Equation environment type (LaTeX-style).
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
554#[serde(rename_all = "lowercase")]
555pub enum EquationEnvironment {
556    /// Align environment (aligned at &).
557    Align,
558    /// Gather environment (centered, no alignment).
559    Gather,
560    /// Multline environment (first line left, last right).
561    Multline,
562    /// Split environment (within equation).
563    Split,
564    /// Cases environment.
565    Cases,
566    /// Alignat environment (multiple alignment points).
567    Alignat,
568}
569
570/// A single equation line in a group.
571#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
572#[serde(rename_all = "camelCase")]
573pub struct EquationLine {
574    /// Optional unique identifier for referencing.
575    #[serde(default, skip_serializing_if = "Option::is_none")]
576    pub id: Option<String>,
577
578    /// Equation content (LaTeX or other notation).
579    pub value: String,
580
581    /// Equation number (auto-generated or explicit).
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub number: Option<String>,
584
585    /// Custom tag instead of a number.
586    #[serde(default, skip_serializing_if = "Option::is_none")]
587    pub tag: Option<String>,
588}
589
590impl EquationLine {
591    /// Create a new equation line.
592    #[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    /// Set the equation number.
603    #[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    /// Set a custom tag.
610    #[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    /// Set the ID.
617    #[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
624// ============================================================================
625// Algorithm
626// ============================================================================
627
628fn default_true() -> bool {
629    true
630}
631
632/// A pseudocode algorithm block.
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635pub struct Algorithm {
636    /// Optional unique identifier.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub id: Option<String>,
639
640    /// Algorithm name/title.
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub name: Option<String>,
643
644    /// Algorithm number.
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub number: Option<String>,
647
648    /// Caption/description.
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub caption: Option<String>,
651
652    /// Input parameters.
653    #[serde(default, skip_serializing_if = "Vec::is_empty")]
654    pub inputs: Vec<AlgorithmParam>,
655
656    /// Output parameters.
657    #[serde(default, skip_serializing_if = "Vec::is_empty")]
658    pub outputs: Vec<AlgorithmParam>,
659
660    /// Algorithm body (pseudocode lines).
661    pub body: Vec<AlgorithmLine>,
662
663    /// Whether to show line numbers.
664    #[serde(default = "default_true")]
665    pub line_numbers: bool,
666
667    /// Starting line number (default: 1).
668    #[serde(default, skip_serializing_if = "Option::is_none")]
669    pub start_line: Option<u32>,
670}
671
672impl Algorithm {
673    /// Create a new algorithm.
674    #[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    /// Set the name.
690    #[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    /// Add an input parameter.
697    #[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    /// Add an output parameter.
707    #[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/// Algorithm input/output parameter.
718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
719pub struct AlgorithmParam {
720    /// Parameter name.
721    pub name: String,
722    /// Parameter description.
723    pub description: String,
724}
725
726/// A line of pseudocode in an algorithm.
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728#[serde(rename_all = "camelCase")]
729pub struct AlgorithmLine {
730    /// Line number (auto-generated if not specified).
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub line_number: Option<u32>,
733
734    /// Indentation level.
735    #[serde(default)]
736    pub indent: u8,
737
738    /// Line type.
739    pub line_type: AlgorithmLineType,
740
741    /// Line content.
742    pub content: String,
743
744    /// Optional comment.
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub comment: Option<String>,
747}
748
749/// Type of algorithm line.
750#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
751#[serde(rename_all = "lowercase")]
752pub enum AlgorithmLineType {
753    /// Regular statement.
754    Statement,
755    /// If condition.
756    If,
757    /// Else if.
758    ElseIf,
759    /// Else.
760    Else,
761    /// End if.
762    EndIf,
763    /// For loop.
764    For,
765    /// End for.
766    EndFor,
767    /// While loop.
768    While,
769    /// End while.
770    EndWhile,
771    /// Function definition.
772    Function,
773    /// End function.
774    EndFunction,
775    /// Return statement.
776    Return,
777    /// Comment line.
778    Comment,
779}
780
781// ============================================================================
782// Cross-Reference Marks
783// ============================================================================
784
785/// An equation reference mark for cross-referencing equations.
786///
787/// Used inline to reference equations defined in equation groups.
788///
789/// # Example JSON
790///
791/// ```json
792/// {
793///   "type": "text",
794///   "value": "(2.5)",
795///   "marks": [
796///     {
797///       "type": "academic:equation-ref",
798///       "target": "#eq-fx",
799///       "format": "({number})"
800///     }
801///   ]
802/// }
803/// ```
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "camelCase")]
806pub struct EquationRef {
807    /// Content Anchor URI to the equation (e.g., "#eq-fx").
808    pub target: String,
809
810    /// Display format with placeholder (default: "({number})").
811    #[serde(default, skip_serializing_if = "Option::is_none")]
812    pub format: Option<String>,
813}
814
815impl EquationRef {
816    /// Create a new equation reference.
817    #[must_use]
818    pub fn new(target: impl Into<String>) -> Self {
819        Self {
820            target: target.into(),
821            format: None,
822        }
823    }
824
825    /// Set a custom format string.
826    ///
827    /// Use `{number}` as a placeholder for the equation number.
828    #[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    /// Convert to an extension mark for use in text.
835    #[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/// An algorithm reference mark for cross-referencing algorithms and their lines.
848///
849/// Used inline to reference algorithms or specific lines within algorithms.
850///
851/// # Example JSON
852///
853/// ```json
854/// {
855///   "type": "text",
856///   "value": "Algorithm 1",
857///   "marks": [
858///     {
859///       "type": "academic:algorithm-ref",
860///       "target": "#alg-quicksort",
861///       "format": "Algorithm {number}"
862///     }
863///   ]
864/// }
865/// ```
866#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
867#[serde(rename_all = "camelCase")]
868pub struct AlgorithmRef {
869    /// Content Anchor URI to the algorithm (e.g., "#alg-quicksort").
870    pub target: String,
871
872    /// Optional line label for line-specific references.
873    #[serde(default, skip_serializing_if = "Option::is_none")]
874    pub line: Option<String>,
875
876    /// Display format with placeholders (e.g., "Algorithm {number}" or "line {line}").
877    #[serde(default, skip_serializing_if = "Option::is_none")]
878    pub format: Option<String>,
879}
880
881impl AlgorithmRef {
882    /// Create a new algorithm reference.
883    #[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    /// Reference a specific line within the algorithm.
893    #[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    /// Set a custom format string.
900    ///
901    /// Use `{number}` for algorithm number, `{line}` for line reference.
902    #[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    /// Convert to an extension mark for use in text.
909    #[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/// A theorem reference mark for cross-referencing theorems, lemmas, etc.
925///
926/// # Example JSON
927///
928/// ```json
929/// {
930///   "type": "text",
931///   "value": "Theorem 3.1",
932///   "marks": [
933///     {
934///       "type": "academic:theorem-ref",
935///       "target": "#thm-pythagoras",
936///       "format": "{variant} {number}"
937///     }
938///   ]
939/// }
940/// ```
941#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
942#[serde(rename_all = "camelCase")]
943pub struct TheoremRef {
944    /// Content Anchor URI to the theorem (e.g., "#thm-pythagoras").
945    pub target: String,
946
947    /// Display format with placeholders.
948    #[serde(default, skip_serializing_if = "Option::is_none")]
949    pub format: Option<String>,
950}
951
952impl TheoremRef {
953    /// Create a new theorem reference.
954    #[must_use]
955    pub fn new(target: impl Into<String>) -> Self {
956        Self {
957            target: target.into(),
958            format: None,
959        }
960    }
961
962    /// Set a custom format string.
963    ///
964    /// Use `{variant}` for theorem type, `{number}` for theorem number.
965    #[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    /// Convert to an extension mark for use in text.
972    #[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// ============================================================================
985// Numbering Configuration
986// ============================================================================
987
988/// Numbering configuration for academic content.
989#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
990#[serde(rename_all = "camelCase")]
991pub struct NumberingConfig {
992    /// Equation numbering configuration.
993    #[serde(default, skip_serializing_if = "Option::is_none")]
994    pub equations: Option<NumberingStyle>,
995
996    /// Theorem numbering configuration.
997    #[serde(default, skip_serializing_if = "Option::is_none")]
998    pub theorems: Option<NumberingStyle>,
999
1000    /// Algorithm numbering configuration.
1001    #[serde(default, skip_serializing_if = "Option::is_none")]
1002    pub algorithms: Option<NumberingStyle>,
1003
1004    /// Figure numbering configuration.
1005    #[serde(default, skip_serializing_if = "Option::is_none")]
1006    pub figures: Option<NumberingStyle>,
1007
1008    /// Table numbering configuration.
1009    #[serde(default, skip_serializing_if = "Option::is_none")]
1010    pub tables: Option<NumberingStyle>,
1011}
1012
1013/// Numbering style configuration.
1014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1015#[serde(rename_all = "camelCase")]
1016pub struct NumberingStyle {
1017    /// Format string (e.g., "{chapter}.{number}").
1018    pub format: String,
1019
1020    /// Whether to reset numbering per chapter.
1021    #[serde(default)]
1022    pub reset_per_chapter: bool,
1023
1024    /// Starting number.
1025    #[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        // Deserialize back
1310        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}