use serde::{Deserialize, Serialize};
use crate::content::Block;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Abstract {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub children: Vec<Block>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sections: Vec<AbstractSection>,
}
impl Abstract {
#[must_use]
pub fn new(children: Vec<Block>) -> Self {
Self {
id: None,
children,
keywords: Vec::new(),
sections: Vec::new(),
}
}
#[must_use]
pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
self.keywords = keywords;
self
}
#[must_use]
pub fn with_section(mut self, section: AbstractSection) -> Self {
self.sections.push(section);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AbstractSection {
pub section_type: AbstractSectionType,
pub children: Vec<Block>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AbstractSectionType {
Background,
Objectives,
Methods,
Results,
Conclusions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Theorem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub variant: TheoremVariant,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
pub children: Vec<Block>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attribution: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub citation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uses: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restate: Option<bool>,
}
impl Theorem {
#[must_use]
pub fn new(variant: TheoremVariant, children: Vec<Block>) -> Self {
Self {
id: None,
variant,
label: None,
number: None,
children,
attribution: None,
citation: None,
uses: None,
restate: None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_number(mut self, number: impl Into<String>) -> Self {
self.number = Some(number.into());
self
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_attribution(mut self, attribution: impl Into<String>) -> Self {
self.attribution = Some(attribution.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "lowercase")]
pub enum TheoremVariant {
Theorem,
Lemma,
Proposition,
Corollary,
Definition,
Conjecture,
Remark,
Example,
Axiom,
Claim,
Fact,
Assumption,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Proof {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theorem_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<ProofMethod>,
pub children: Vec<Block>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub qed_symbol: Option<String>,
}
impl Proof {
#[must_use]
pub fn new(children: Vec<Block>) -> Self {
Self {
id: None,
theorem_ref: None,
method: None,
children,
qed_symbol: None,
}
}
#[must_use]
pub fn of_theorem(mut self, theorem_id: impl Into<String>) -> Self {
self.theorem_ref = Some(theorem_id.into());
self
}
#[must_use]
pub fn with_method(mut self, method: ProofMethod) -> Self {
self.method = Some(method);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProofMethod {
Direct,
Contradiction,
Contrapositive,
Induction,
StrongInduction,
Cases,
Constructive,
Existence,
Uniqueness,
Sketch,
StructuralInduction,
Counting,
Probabilistic,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Exercise {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub difficulty: Option<Difficulty>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub points: Option<u32>,
pub children: Vec<Block>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parts: Vec<ExercisePart>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hints: Vec<Block>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub solution: Option<Solution>,
}
impl Exercise {
#[must_use]
pub fn new(children: Vec<Block>) -> Self {
Self {
id: None,
number: None,
difficulty: None,
points: None,
children,
parts: Vec::new(),
hints: Vec::new(),
solution: None,
}
}
#[must_use]
pub fn with_number(mut self, number: impl Into<String>) -> Self {
self.number = Some(number.into());
self
}
#[must_use]
pub fn with_difficulty(mut self, difficulty: Difficulty) -> Self {
self.difficulty = Some(difficulty);
self
}
#[must_use]
pub fn with_points(mut self, points: u32) -> Self {
self.points = Some(points);
self
}
#[must_use]
pub fn with_part(mut self, part: ExercisePart) -> Self {
self.parts.push(part);
self
}
#[must_use]
pub fn with_hint(mut self, hint: Vec<Block>) -> Self {
self.hints.extend(hint);
self
}
#[must_use]
pub fn with_solution(mut self, solution: Solution) -> Self {
self.solution = Some(solution);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Difficulty {
Easy,
Medium,
Hard,
Challenge,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExercisePart {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub points: Option<u32>,
pub children: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Solution {
#[serde(default)]
pub hidden: bool,
pub children: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExerciseSet {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context: Vec<Block>,
pub exercises: Vec<Exercise>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_points: Option<u32>,
}
impl ExerciseSet {
#[must_use]
pub fn new(exercises: Vec<Exercise>) -> Self {
Self {
id: None,
title: None,
context: Vec::new(),
exercises,
total_points: None,
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_context(mut self, context: Vec<Block>) -> Self {
self.context = context;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EquationGroup {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub environment: EquationEnvironment,
pub lines: Vec<EquationLine>,
}
impl EquationGroup {
#[must_use]
pub fn new(environment: EquationEnvironment, lines: Vec<EquationLine>) -> Self {
Self {
id: None,
environment,
lines,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EquationEnvironment {
Align,
Gather,
Multline,
Split,
Cases,
Alignat,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EquationLine {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
}
impl EquationLine {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self {
id: None,
value: value.into(),
number: None,
tag: None,
}
}
#[must_use]
pub fn with_number(mut self, number: impl Into<String>) -> Self {
self.number = Some(number.into());
self
}
#[must_use]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Algorithm {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caption: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<AlgorithmParam>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub outputs: Vec<AlgorithmParam>,
pub body: Vec<AlgorithmLine>,
#[serde(default = "default_true")]
pub line_numbers: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_line: Option<u32>,
}
impl Algorithm {
#[must_use]
pub fn new(body: Vec<AlgorithmLine>) -> Self {
Self {
id: None,
name: None,
number: None,
caption: None,
inputs: Vec::new(),
outputs: Vec::new(),
body,
line_numbers: true,
start_line: None,
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn with_input(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.inputs.push(AlgorithmParam {
name: name.into(),
description: description.into(),
});
self
}
#[must_use]
pub fn with_output(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.outputs.push(AlgorithmParam {
name: name.into(),
description: description.into(),
});
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AlgorithmParam {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AlgorithmLine {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line_number: Option<u32>,
#[serde(default)]
pub indent: u8,
pub line_type: AlgorithmLineType,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AlgorithmLineType {
Statement,
If,
ElseIf,
Else,
EndIf,
For,
EndFor,
While,
EndWhile,
Function,
EndFunction,
Return,
Comment,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EquationRef {
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
impl EquationRef {
#[must_use]
pub fn new(target: impl Into<String>) -> Self {
Self {
target: target.into(),
format: None,
}
}
#[must_use]
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
let mut attrs = serde_json::json!({
"target": self.target
});
if let Some(ref fmt) = self.format {
attrs["format"] = serde_json::Value::String(fmt.clone());
}
crate::content::ExtensionMark::new("academic", "equation-ref").with_attributes(attrs)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AlgorithmRef {
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
impl AlgorithmRef {
#[must_use]
pub fn new(target: impl Into<String>) -> Self {
Self {
target: target.into(),
line: None,
format: None,
}
}
#[must_use]
pub fn with_line(mut self, line: impl Into<String>) -> Self {
self.line = Some(line.into());
self
}
#[must_use]
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
let mut attrs = serde_json::json!({
"target": self.target
});
if let Some(ref line) = self.line {
attrs["line"] = serde_json::Value::String(line.clone());
}
if let Some(ref fmt) = self.format {
attrs["format"] = serde_json::Value::String(fmt.clone());
}
crate::content::ExtensionMark::new("academic", "algorithm-ref").with_attributes(attrs)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TheoremRef {
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
impl TheoremRef {
#[must_use]
pub fn new(target: impl Into<String>) -> Self {
Self {
target: target.into(),
format: None,
}
}
#[must_use]
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
let mut attrs = serde_json::json!({
"target": self.target
});
if let Some(ref fmt) = self.format {
attrs["format"] = serde_json::Value::String(fmt.clone());
}
crate::content::ExtensionMark::new("academic", "theorem-ref").with_attributes(attrs)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ResetTrigger {
Heading1,
Heading2,
Heading3,
Heading4,
Heading5,
Heading6,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NumberingStylePattern {
#[serde(rename = "number")]
Number,
#[serde(rename = "chapter.number")]
ChapterNumber,
#[serde(rename = "section.number")]
SectionNumber,
#[serde(rename = "chapter.section.number")]
ChapterSectionNumber,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NumberingConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equations: Option<NumberingStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theorems: Option<NumberingStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub algorithms: Option<NumberingStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub figures: Option<NumberingStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tables: Option<NumberingStyle>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NumberingStyle {
#[serde(default, skip_serializing_if = "Option::is_none", alias = "format")]
pub style: Option<NumberingStylePattern>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reset_on: Option<ResetTrigger>,
#[serde(default = "default_start")]
pub start: u32,
}
fn default_start() -> u32 {
1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theorem_new() {
let thm = Theorem::new(TheoremVariant::Theorem, vec![]);
assert_eq!(thm.variant, TheoremVariant::Theorem);
assert!(thm.label.is_none());
assert!(thm.number.is_none());
}
#[test]
fn test_theorem_builder() {
let thm = Theorem::new(TheoremVariant::Lemma, vec![])
.with_label("Pumping Lemma")
.with_number("4.2")
.with_id("pumping")
.with_attribution("Bar-Hillel et al.");
assert_eq!(thm.variant, TheoremVariant::Lemma);
assert_eq!(thm.label, Some("Pumping Lemma".to_string()));
assert_eq!(thm.number, Some("4.2".to_string()));
assert_eq!(thm.id, Some("pumping".to_string()));
assert_eq!(thm.attribution, Some("Bar-Hillel et al.".to_string()));
}
#[test]
fn test_theorem_variant_display() {
assert_eq!(TheoremVariant::Theorem.to_string(), "Theorem");
assert_eq!(TheoremVariant::Lemma.to_string(), "Lemma");
assert_eq!(TheoremVariant::Corollary.to_string(), "Corollary");
}
#[test]
fn test_theorem_serialization() {
let thm = Theorem::new(TheoremVariant::Definition, vec![])
.with_label("Continuity")
.with_number("2.1");
let json = serde_json::to_string(&thm).unwrap();
assert!(json.contains("\"variant\":\"definition\""));
assert!(json.contains("\"label\":\"Continuity\""));
assert!(json.contains("\"number\":\"2.1\""));
}
#[test]
fn test_proof_new() {
let proof = Proof::new(vec![]);
assert!(proof.theorem_ref.is_none());
assert!(proof.method.is_none());
}
#[test]
fn test_proof_builder() {
let proof = Proof::new(vec![])
.of_theorem("thm-pythagoras")
.with_method(ProofMethod::Direct);
assert_eq!(proof.theorem_ref, Some("thm-pythagoras".to_string()));
assert_eq!(proof.method, Some(ProofMethod::Direct));
}
#[test]
fn test_exercise_new() {
let ex = Exercise::new(vec![]);
assert!(ex.number.is_none());
assert!(ex.difficulty.is_none());
}
#[test]
fn test_exercise_builder() {
let ex = Exercise::new(vec![])
.with_number("3.5")
.with_difficulty(Difficulty::Hard)
.with_points(10);
assert_eq!(ex.number, Some("3.5".to_string()));
assert_eq!(ex.difficulty, Some(Difficulty::Hard));
assert_eq!(ex.points, Some(10));
}
#[test]
fn test_algorithm_new() {
let alg = Algorithm::new(vec![]);
assert!(alg.name.is_none());
assert!(alg.line_numbers);
}
#[test]
fn test_algorithm_builder() {
let alg = Algorithm::new(vec![])
.with_name("QuickSort")
.with_input("A", "array to sort")
.with_output("A", "sorted array");
assert_eq!(alg.name, Some("QuickSort".to_string()));
assert_eq!(alg.inputs.len(), 1);
assert_eq!(alg.inputs[0].name, "A");
assert_eq!(alg.outputs.len(), 1);
}
#[test]
fn test_equation_group() {
let line = EquationLine::new("E = mc^2")
.with_id("eq1")
.with_number("(1)");
let group = EquationGroup::new(EquationEnvironment::Align, vec![line]);
assert_eq!(group.environment, EquationEnvironment::Align);
assert_eq!(group.lines.len(), 1);
assert_eq!(group.lines[0].value, "E = mc^2");
assert_eq!(group.lines[0].id, Some("eq1".to_string()));
assert_eq!(group.lines[0].number, Some("(1)".to_string()));
}
#[test]
fn test_equation_line_with_tag() {
let line = EquationLine::new("a^2 + b^2 = c^2").with_tag("*");
assert_eq!(line.tag, Some("*".to_string()));
assert!(line.number.is_none());
}
#[test]
fn test_equation_line_serde_roundtrip() {
let line = EquationLine::new("f(x) = ax + b")
.with_id("eq-fx")
.with_number("2.1")
.with_tag("linear");
let json = serde_json::to_string(&line).unwrap();
assert!(json.contains("\"value\":\"f(x) = ax + b\""));
assert!(json.contains("\"number\":\"2.1\""));
assert!(json.contains("\"tag\":\"linear\""));
let parsed: EquationLine = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.value, "f(x) = ax + b");
assert_eq!(parsed.number, Some("2.1".to_string()));
assert_eq!(parsed.tag, Some("linear".to_string()));
}
#[test]
fn test_equation_line_without_tag_defaults_to_none() {
let json = r#"{"value": "x + y"}"#;
let line: EquationLine = serde_json::from_str(json).unwrap();
assert!(line.tag.is_none());
assert!(line.number.is_none());
assert!(line.id.is_none());
}
#[test]
fn test_equation_group_with_lines_serde() {
let group = EquationGroup::new(
EquationEnvironment::Gather,
vec![
EquationLine::new("a = b").with_number("1"),
EquationLine::new("c = d").with_number("2"),
],
)
.with_id("eq-group-1");
let json = serde_json::to_string(&group).unwrap();
assert!(json.contains("\"lines\""));
assert!(json.contains("\"environment\":\"gather\""));
let parsed: EquationGroup = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.lines.len(), 2);
assert_eq!(parsed.id, Some("eq-group-1".to_string()));
}
#[test]
fn test_alignat_environment_serialization() {
let group = EquationGroup::new(
EquationEnvironment::Alignat,
vec![EquationLine::new("x &= y &= z")],
);
let json = serde_json::to_string(&group).unwrap();
assert!(json.contains("\"environment\":\"alignat\""));
let parsed: EquationGroup = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.environment, EquationEnvironment::Alignat);
}
#[test]
fn test_abstract_new() {
let abs = Abstract::new(vec![])
.with_keywords(vec!["AI".to_string(), "Machine Learning".to_string()]);
assert_eq!(abs.keywords.len(), 2);
assert!(abs.sections.is_empty());
}
#[test]
fn test_equation_ref() {
let eq_ref = EquationRef::new("#eq-pythagoras");
assert_eq!(eq_ref.target, "#eq-pythagoras");
assert!(eq_ref.format.is_none());
let eq_ref_fmt = eq_ref.with_format("Equation ({number})");
assert_eq!(eq_ref_fmt.format, Some("Equation ({number})".to_string()));
}
#[test]
fn test_equation_ref_to_mark() {
let eq_ref = EquationRef::new("#eq-1").with_format("({number})");
let mark = eq_ref.to_extension_mark();
assert_eq!(mark.namespace, "academic");
assert_eq!(mark.mark_type, "equation-ref");
assert_eq!(mark.get_string_attribute("target"), Some("#eq-1"));
assert_eq!(mark.get_string_attribute("format"), Some("({number})"));
}
#[test]
fn test_algorithm_ref() {
let alg_ref = AlgorithmRef::new("#alg-quicksort");
assert_eq!(alg_ref.target, "#alg-quicksort");
assert!(alg_ref.line.is_none());
assert!(alg_ref.format.is_none());
}
#[test]
fn test_algorithm_ref_with_line() {
let alg_ref = AlgorithmRef::new("#alg-bisection")
.with_line("loop")
.with_format("line {line}");
assert_eq!(alg_ref.target, "#alg-bisection");
assert_eq!(alg_ref.line, Some("loop".to_string()));
assert_eq!(alg_ref.format, Some("line {line}".to_string()));
}
#[test]
fn test_algorithm_ref_to_mark() {
let alg_ref = AlgorithmRef::new("#alg-1")
.with_line("start")
.with_format("Algorithm {number}, line {line}");
let mark = alg_ref.to_extension_mark();
assert_eq!(mark.namespace, "academic");
assert_eq!(mark.mark_type, "algorithm-ref");
assert_eq!(mark.get_string_attribute("target"), Some("#alg-1"));
assert_eq!(mark.get_string_attribute("line"), Some("start"));
assert_eq!(
mark.get_string_attribute("format"),
Some("Algorithm {number}, line {line}")
);
}
#[test]
fn test_theorem_ref() {
let thm_ref = TheoremRef::new("#thm-pythagoras");
assert_eq!(thm_ref.target, "#thm-pythagoras");
assert!(thm_ref.format.is_none());
}
#[test]
fn test_theorem_ref_to_mark() {
let thm_ref = TheoremRef::new("#thm-1").with_format("{variant} {number}");
let mark = thm_ref.to_extension_mark();
assert_eq!(mark.namespace, "academic");
assert_eq!(mark.mark_type, "theorem-ref");
assert_eq!(mark.get_string_attribute("target"), Some("#thm-1"));
assert_eq!(
mark.get_string_attribute("format"),
Some("{variant} {number}")
);
}
#[test]
fn test_equation_ref_serialization() {
let eq_ref = EquationRef::new("#eq-fx").with_format("({number})");
let json = serde_json::to_string(&eq_ref).unwrap();
assert!(json.contains("\"target\":\"#eq-fx\""));
assert!(json.contains("\"format\":\"({number})\""));
let parsed: EquationRef = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.target, "#eq-fx");
assert_eq!(parsed.format, Some("({number})".to_string()));
}
#[test]
fn test_algorithm_ref_serialization() {
let alg_ref = AlgorithmRef::new("#alg-sort")
.with_line("pivot")
.with_format("line {line}");
let json = serde_json::to_string(&alg_ref).unwrap();
assert!(json.contains("\"target\":\"#alg-sort\""));
assert!(json.contains("\"line\":\"pivot\""));
assert!(json.contains("\"format\":\"line {line}\""));
}
#[test]
fn test_theorem_uses_and_restate_roundtrip() {
let thm = Theorem {
id: Some("thm-2".to_string()),
variant: TheoremVariant::Corollary,
label: None,
number: None,
children: vec![],
attribution: None,
citation: None,
uses: Some(vec!["#thm-1".to_string(), "#lemma-1".to_string()]),
restate: Some(true),
};
let json = serde_json::to_string(&thm).unwrap();
assert!(json.contains("\"uses\":[\"#thm-1\",\"#lemma-1\"]"));
assert!(json.contains("\"restate\":true"));
let parsed: Theorem = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.uses,
Some(vec!["#thm-1".to_string(), "#lemma-1".to_string()])
);
assert_eq!(parsed.restate, Some(true));
}
#[test]
fn test_theorem_without_new_fields_defaults_to_none() {
let json = r#"{
"variant": "theorem",
"children": []
}"#;
let thm: Theorem = serde_json::from_str(json).unwrap();
assert!(thm.uses.is_none());
assert!(thm.restate.is_none());
}
#[test]
fn test_new_proof_method_variants() {
let methods = [
(ProofMethod::StructuralInduction, "structuralinduction"),
(ProofMethod::Counting, "counting"),
(ProofMethod::Probabilistic, "probabilistic"),
];
for (method, expected_str) in methods {
let json = serde_json::to_string(&method).unwrap();
assert_eq!(json, format!("\"{expected_str}\""));
let parsed: ProofMethod = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, method);
}
}
#[test]
fn test_algorithm_start_line_roundtrip() {
let alg = Algorithm {
id: None,
name: Some("BFS".to_string()),
number: None,
caption: None,
inputs: Vec::new(),
outputs: Vec::new(),
body: vec![],
line_numbers: true,
start_line: Some(10),
};
let json = serde_json::to_string(&alg).unwrap();
assert!(json.contains("\"startLine\":10"));
let parsed: Algorithm = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.start_line, Some(10));
}
#[test]
fn test_algorithm_without_start_line_defaults_to_none() {
let json = r#"{
"body": [],
"lineNumbers": true
}"#;
let alg: Algorithm = serde_json::from_str(json).unwrap();
assert!(alg.start_line.is_none());
}
}