use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CmlDocument {
pub version: String,
pub encoding: String,
pub profile: String,
pub id: Option<String>,
pub header: Header,
pub body: Body,
pub footer: Footer,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Header {
pub title: String,
pub authors: Vec<Author>,
pub dates: Vec<DateEntry>,
pub identifiers: Vec<Identifier>,
pub version: Option<String>,
pub description: Option<String>,
pub provenance: Option<String>,
pub source: Option<String>,
pub meta: Vec<MetaEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Author {
pub name: String,
pub role: Option<String>,
pub reference: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DateEntry {
pub date_type: String,
pub when: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Identifier {
pub scheme: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MetaEntry {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Body {
pub blocks: Vec<BlockElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BlockElement {
Section(Section),
Paragraph(Paragraph),
Heading(Heading),
Aside(Aside),
Quote(Quote),
List(List),
Table(Table),
Code(Code),
Break(Break),
Figure(Figure),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Section {
pub id: Option<String>,
pub section_type: Option<String>,
pub reference: Option<String>,
pub content: Vec<BlockElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Paragraph {
pub id: Option<String>,
pub paragraph_type: Option<String>,
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Heading {
pub id: Option<String>,
pub heading_type: Option<String>,
pub size: u8,
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Aside {
pub id: Option<String>,
pub aside_type: Option<String>,
pub side: Side,
pub content: Vec<BlockElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Side {
Left,
Right,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Quote {
pub id: Option<String>,
pub reference: Option<String>,
pub source: Option<String>,
pub content: Vec<BlockElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct List {
pub id: Option<String>,
pub list_type: Option<ListType>,
pub style: Option<ListStyle>,
pub items: Vec<ListItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListType {
Ordered,
Unordered,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListStyle {
Numeric,
Roman,
Alpha,
Symbolic,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ListItem {
pub id: Option<String>,
pub content: ListItemContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListItemContent {
Inline(Vec<InlineElement>),
Block(Vec<BlockElement>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Table {
pub id: Option<String>,
pub table_type: Option<String>,
pub header: Option<TableHeader>,
pub body: TableBody,
pub footer: Option<TableFooter>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableHeader {
pub rows: Vec<TableRow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableBody {
pub rows: Vec<TableRow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableFooter {
pub caption: Caption,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableRow {
pub columns: Vec<TableColumn>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableColumn {
pub sort: Option<SortOrder>,
pub cell: TableCell,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableCell {
pub colspan: Option<u32>,
pub rowspan: Option<u32>,
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Caption {
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Code {
pub id: Option<String>,
pub lang: Option<String>,
pub copyable: Option<bool>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Break {
pub break_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Figure {
pub id: Option<String>,
pub figure_type: Option<String>,
pub reference: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InlineElement {
Text(String),
Em(Em),
Bo(Bo),
Un(Un),
St(St),
Snip(Snip),
Key(Key),
Rf(Rf),
Tg(Tg),
Lk(Lk),
Curr(Curr),
End(End),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Em {
pub em_type: Option<EmphasisType>,
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EmphasisType {
Stress,
Contrast,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Bo {
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Un {
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct St {
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Snip {
pub char: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Key {
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Rf {
pub reference: String,
pub role: Option<String>,
pub title: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Tg {
pub reference: String,
pub role: Option<String>,
pub title: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Lk {
pub reference: String,
pub role: Option<String>,
pub title: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Curr {
pub currency_type: String,
pub format: Option<CurrencyFormat>,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CurrencyFormat {
Symbol,
Code,
Name,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct End {
pub kind: Option<EndKind>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EndKind {
Line,
Verse,
Item,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Footer {
pub signatures: Option<Signatures>,
pub citations: Option<Citations>,
pub annotations: Option<Annotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Signatures {
pub signatures: Vec<Signature>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Signature {
pub when: String,
pub role: Option<String>,
pub reference: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Citations {
pub citations: Vec<Citation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Citation {
pub reference: String,
pub citation_type: Option<String>,
pub content: Vec<InlineElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Annotations {
pub notes: Vec<Note>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Note {
pub id: Option<String>,
pub note_type: Option<String>,
pub reference: Option<String>,
pub content: NoteContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NoteContent {
Inline(Vec<InlineElement>),
Block(Vec<BlockElement>),
}
impl CmlDocument {
pub fn new(profile: String, header: Header) -> Self {
Self {
version: "0.2".to_string(),
encoding: "utf-8".to_string(),
profile,
id: None,
header,
body: Body { blocks: Vec::new() },
footer: Footer::empty(),
}
}
pub fn with_id(mut self, id: String) -> Self {
self.id = Some(id);
self
}
pub fn with_body(mut self, body: Body) -> Self {
self.body = body;
self
}
pub fn with_footer(mut self, footer: Footer) -> Self {
self.footer = footer;
self
}
}
impl Header {
pub fn new(title: String) -> Self {
Self {
title,
authors: Vec::new(),
dates: Vec::new(),
identifiers: Vec::new(),
version: None,
description: None,
provenance: None,
source: None,
meta: Vec::new(),
}
}
pub fn add_author(&mut self, author: Author) {
self.authors.push(author);
}
pub fn add_date(&mut self, date: DateEntry) {
self.dates.push(date);
}
pub fn add_identifier(&mut self, identifier: Identifier) {
self.identifiers.push(identifier);
}
pub fn with_version(mut self, version: String) -> Self {
self.version = Some(version);
self
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn with_provenance(mut self, provenance: String) -> Self {
self.provenance = Some(provenance);
self
}
pub fn with_source(mut self, source: String) -> Self {
self.source = Some(source);
self
}
pub fn add_meta(&mut self, name: String, value: String) {
self.meta.push(MetaEntry { name, value });
}
}
impl Author {
pub fn new(name: String) -> Self {
Self {
name,
role: None,
reference: None,
}
}
pub fn with_role(mut self, role: String) -> Self {
self.role = Some(role);
self
}
pub fn with_reference(mut self, reference: String) -> Self {
self.reference = Some(reference);
self
}
}
impl DateEntry {
pub fn new(date_type: String, when: String) -> Self {
Self { date_type, when }
}
pub fn created(when: String) -> Self {
Self::new("created".to_string(), when)
}
pub fn modified(when: String) -> Self {
Self::new("modified".to_string(), when)
}
pub fn published(when: String) -> Self {
Self::new("published".to_string(), when)
}
}
impl Identifier {
pub fn new(scheme: String, value: String) -> Self {
Self { scheme, value }
}
pub fn continuity(value: String) -> Self {
Self::new("continuity".to_string(), value)
}
pub fn doi(value: String) -> Self {
Self::new("doi".to_string(), value)
}
}
impl Body {
pub fn new() -> Self {
Self { blocks: Vec::new() }
}
pub fn add_block(&mut self, block: BlockElement) {
self.blocks.push(block);
}
}
impl Default for Body {
fn default() -> Self {
Self::new()
}
}
impl Footer {
pub fn empty() -> Self {
Self {
signatures: None,
citations: None,
annotations: None,
}
}
pub fn with_signatures(mut self, signatures: Signatures) -> Self {
self.signatures = Some(signatures);
self
}
pub fn with_citations(mut self, citations: Citations) -> Self {
self.citations = Some(citations);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
impl Signature {
pub fn new(when: String, content: String) -> Self {
Self {
when,
role: None,
reference: None,
content,
}
}
pub fn with_role(mut self, role: String) -> Self {
self.role = Some(role);
self
}
pub fn with_reference(mut self, reference: String) -> Self {
self.reference = Some(reference);
self
}
}
impl Paragraph {
pub fn new(content: Vec<InlineElement>) -> Self {
Self {
id: None,
paragraph_type: None,
content,
}
}
pub fn from_text(text: String) -> Self {
Self {
id: None,
paragraph_type: None,
content: vec![InlineElement::Text(text)],
}
}
}
impl Heading {
pub fn new(size: u8, content: Vec<InlineElement>) -> Self {
Self {
id: None,
heading_type: None,
size,
content,
}
}
pub fn from_text(size: u8, text: String) -> Self {
Self {
id: None,
heading_type: None,
size,
content: vec![InlineElement::Text(text)],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minimal_document() {
let header = Header::new("Test Document".to_string());
let doc = CmlDocument::new("core".to_string(), header);
assert_eq!(doc.version, "0.2");
assert_eq!(doc.encoding, "utf-8");
assert_eq!(doc.profile, "core");
assert_eq!(doc.header.title, "Test Document");
}
#[test]
fn test_header_with_metadata() {
let mut header = Header::new("Test".to_string());
header.add_author(Author::new("John Doe".to_string()));
header.add_date(DateEntry::created("2025-12-22".to_string()));
header.add_identifier(Identifier::continuity("test-doc-123".to_string()));
header.add_meta("status".to_string(), "draft".to_string());
assert_eq!(header.authors.len(), 1);
assert_eq!(header.dates.len(), 1);
assert_eq!(header.identifiers.len(), 1);
assert_eq!(header.meta.len(), 1);
}
#[test]
fn test_body_with_blocks() {
let mut body = Body::new();
body.add_block(BlockElement::Paragraph(Paragraph::from_text(
"Hello, world!".to_string(),
)));
body.add_block(BlockElement::Heading(Heading::from_text(
1,
"Title".to_string(),
)));
assert_eq!(body.blocks.len(), 2);
}
#[test]
fn test_footer_with_signatures() {
let signature = Signature::new("2025-12-22T10:30:00Z".to_string(), "John Doe".to_string())
.with_role("author".to_string());
let signatures = Signatures {
signatures: vec![signature],
};
let footer = Footer::empty().with_signatures(signatures);
assert!(footer.signatures.is_some());
assert_eq!(footer.signatures.unwrap().signatures.len(), 1);
}
#[test]
fn test_inline_elements() {
let inline = vec![
InlineElement::Text("This is ".to_string()),
InlineElement::Em(Em {
em_type: None,
content: vec![InlineElement::Text("emphasized".to_string())],
}),
InlineElement::Text(" text.".to_string()),
];
assert_eq!(inline.len(), 3);
}
#[test]
fn test_list_structure() {
let list = List {
id: None,
list_type: Some(ListType::Ordered),
style: Some(ListStyle::Numeric),
items: vec![
ListItem {
id: None,
content: ListItemContent::Inline(vec![InlineElement::Text(
"First".to_string(),
)]),
},
ListItem {
id: None,
content: ListItemContent::Inline(vec![InlineElement::Text(
"Second".to_string(),
)]),
},
],
};
assert_eq!(list.items.len(), 2);
}
#[test]
fn test_table_structure() {
let table = Table {
id: None,
table_type: None,
header: None,
body: TableBody {
rows: vec![TableRow {
columns: vec![TableColumn {
sort: None,
cell: TableCell {
colspan: None,
rowspan: None,
content: vec![InlineElement::Text("Cell".to_string())],
},
}],
}],
},
footer: None,
};
assert_eq!(table.body.rows.len(), 1);
}
}