use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Document {
pub blocks: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Block {
Heading {
level: u8,
title: Title,
tags: Vec<Tag>,
children: Vec<Block>,
},
Paragraph {
inlines: Vec<Inline>,
},
SrcBlock {
language: String,
content: String,
},
ExampleBlock {
content: String,
},
QuoteBlock {
children: Vec<Block>,
},
List {
list_type: ListType,
items: Vec<ListItem>,
},
Table {
rows: Vec<Vec<TableCell>>,
},
PropertyDrawer {
entries: Vec<(String, String)>,
},
LogbookDrawer {
entries: Vec<LogEntry>,
},
Planning {
entries: Vec<PlanningEntry>,
},
Comment {
text: String,
},
Keyword {
name: String,
value: String,
},
BlankLine,
HorizontalRule,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ListType {
Ordered(u64),
Unordered,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListItem {
pub content: Vec<Block>,
pub checkbox: Checkbox,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Checkbox {
NoCheckbox,
Unchecked,
Checked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableCell {
pub inlines: Vec<Inline>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PlanningEntry {
Scheduled(Timestamp),
Deadline(Timestamp),
Closed(Timestamp),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogEntry {
pub timestamp: Timestamp,
pub note: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Inline {
Plain(String),
Bold(Vec<Inline>),
Italic(Vec<Inline>),
Strikethrough(Vec<Inline>),
InlineCode(String),
Verbatim(String),
LineBreak,
Link {
target: String,
description: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NodeId(pub String);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Title(pub String);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Tag(pub String);
impl NodeId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Title {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Tag {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for NodeId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for NodeId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<String> for Title {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Title {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<String> for Tag {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Tag {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::fmt::Display for Title {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::fmt::Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Timestamp(pub String);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construct_simple_document() {
let doc = Document {
blocks: vec![Block::Heading {
level: 1,
title: Title("Hello".into()),
tags: vec![],
children: vec![Block::Paragraph {
inlines: vec![Inline::Plain("some text".into())],
}],
}],
};
assert_eq!(doc.blocks.len(), 1);
}
#[test]
fn ast_types_serialize_roundtrip() {
let doc = Document {
blocks: vec![
Block::Heading {
level: 1,
title: Title("Test".into()),
tags: vec![Tag("rust".into())],
children: vec![
Block::Paragraph {
inlines: vec![
Inline::Plain("Hello ".into()),
Inline::Bold(vec![Inline::Plain("world".into())]),
Inline::Plain(". ".into()),
Inline::Link {
target: "https://example.com".into(),
description: Some("link".into()),
},
],
},
Block::SrcBlock {
language: "rust".into(),
content: "fn main() {}".into(),
},
Block::PropertyDrawer {
entries: vec![("ID".into(), "abc-123".into())],
},
],
},
Block::List {
list_type: ListType::Unordered,
items: vec![
ListItem {
content: vec![Block::Paragraph {
inlines: vec![Inline::Plain("first".into())],
}],
checkbox: Checkbox::NoCheckbox,
},
ListItem {
content: vec![Block::Paragraph {
inlines: vec![Inline::Plain("second".into())],
}],
checkbox: Checkbox::Checked,
},
],
},
],
};
let json = serde_json::to_string(&doc).unwrap();
let roundtripped: Document = serde_json::from_str(&json).unwrap();
assert_eq!(doc, roundtripped);
}
}