From b546c9cdd7bc3daa41d62d9a885b32d6cd3f85d6 Mon Sep 17 00:00:00 2001 From: Serhiy Barhamon Date: Fri, 3 Nov 2023 00:18:03 +0200 Subject: [PATCH] YAML metadata (#40) --- Cargo.toml | 1 + src/lib.rs | 19 +++--- src/nodes/metadata.rs | 143 +++++++++++------------------------------- src/nodes/yamd.rs | 23 ++++--- 4 files changed, 65 insertions(+), 121 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1311180..d30f2dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ pretty_assertions = "1.4.0" [dependencies] serde = { version = "1.0.190", features = ["derive"] } chrono = { version = "0.4.31", features = ["serde"] } +serde_yaml = "0.9.27" diff --git a/src/lib.rs b/src/lib.rs index 96abb23..d4fd7eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,21 +4,24 @@ //! //! ## Syntax //! -//! Each yamd document starts with metadata section that can contain document header, timestamp, image, preview, and -//! tags. Metadata section ends with "^^^\n\n" and can be omitted. +//! Each yamd document starts with metadata section which is YAML document surrounded by "---". Metadata has five +//! fields: title, date, image, preview, and tags. //! -//! Timestamp format: "%Y-%m-%d %H:%M:%S %z" ([specifiers description](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)) +//! Timestamp format: "%Y-%m-%dT%H:%M:%S%z" ([specifiers description](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)) //! -//! Tags are comma separated list. +//! Tags are array of strings. //! //! Example: //! ```text -//! header: Yamd - yet another markdown document flavour -//! timestamp: 2023-01-01 00:00:00 +0000 +//! --- +//! title: Yamd - yet another markdown document flavour +//! date: 2023-01-01 00:00:00 +0000 //! image: /image.png //! preview: Here you can find out more about yamd -//! tags: markdown, rust -//! ^^^ +//! tags: +//! - markdown +//! - rust +//! --- //! //! ``` //! diff --git a/src/nodes/metadata.rs b/src/nodes/metadata.rs index 448abcc..278d437 100644 --- a/src/nodes/metadata.rs +++ b/src/nodes/metadata.rs @@ -1,16 +1,21 @@ use std::fmt::Display; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{matcher::Matcher, node::Node}; use chrono::{DateTime, FixedOffset}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Serialize, Default, Clone)] +#[derive(Debug, PartialEq, Serialize, Default, Clone, Deserialize)] pub struct Metadata { + #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, - pub timestamp: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub date: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub image: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub preview: Option, - pub tags: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, } impl Metadata { @@ -23,111 +28,51 @@ impl Metadata { ) -> Self { Self { title: title.map(|h| h.into()), - timestamp, + date: timestamp, image: image.map(|i| i.into()), preview: preview.map(|p| p.into()), - tags: tags.unwrap_or_default(), + tags, } } -} -impl Display for Metadata { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.len() == 0 { - return write!(f, ""); + pub fn deserialize(input: &str) -> Option { + let mut matcher = Matcher::new(input); + if let Some(metadata) = matcher.get_match("---\n", "---", false) { + let meta: Metadata = serde_yaml::from_str(metadata.body).unwrap_or_else(|e| { + panic!("Failed to deserialize metadata: {}\n{}\n", metadata.body, e) + }); + return Some(meta); } - write!( - f, - "{}{}{}{}{}^^^\n\n", - self.title - .as_ref() - .map_or("".to_string(), |h| format!("title: {h}\n")), - self.timestamp - .as_ref() - .map_or("".to_string(), |t| format!("timestamp: {t}\n")), - self.image - .as_ref() - .map_or("".to_string(), |i| format!("image: {i}\n")), - self.preview - .as_ref() - .map_or("".to_string(), |p| format!("preview: {p}\n")), - if self.tags.is_empty() { - "".to_string() - } else { - format!("tags: {}\n", self.tags.join(", ")) - }, - ) + + None } } -impl Node for Metadata { - fn len(&self) -> usize { - let len = self.title.as_ref().map_or(0, |h| h.len() + 8) - + self - .timestamp - .as_ref() - .map_or(0, |t| t.to_string().len() + 12) - + self.image.as_ref().map_or(0, |i| i.len() + 8) - + self.preview.as_ref().map_or(0, |p| p.len() + 10) - + if self.tags.is_empty() { - 0 - } else { - self.tags.iter().map(|tag| tag.len()).sum::() - + 7 - + if self.tags.len() > 1 { - (self.tags.len() - 1) * 2 - } else { - 0 - } - }; - if len > 0 { - len + 5 +impl Display for Metadata { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.title.is_none() + && self.date.is_none() + && self.image.is_none() + && self.preview.is_none() + && self.tags.is_none() + { + Ok(()) } else { - 0 + write!(f, "---\n{}---", serde_yaml::to_string(self).unwrap()) } } } -impl Deserializer for Metadata { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(metadata) = matcher.get_match("", "^^^\n\n", false) { - let mut meta = Self::new::<&str>(None, None, None, None, None); - metadata.body.split('\n').for_each(|line| { - if line.starts_with("title: ") { - meta.title = Some(line.replace("title: ", "")); - } else if line.starts_with("timestamp: ") { - // remove this when %:z works as expected - if line.trim().chars().rev().nth(5) == Some('+') { - meta.timestamp = DateTime::parse_from_str( - line.replace("timestamp: ", "").as_str(), - // %:z does not work as expected - // https://github.com/chronotope/chrono/pull/1083 - "%Y-%m-%d %H:%M:%S %:z", - ) - .ok(); - } - } else if line.starts_with("image: ") { - meta.image = Some(line.replace("image: ", "")); - } else if line.starts_with("preview: ") { - meta.preview = Some(line.replace("preview: ", "")); - } else if line.starts_with("tags: ") { - meta.tags = line - .replace("tags: ", "") - .split(", ") - .map(|tag| tag.to_string()) - .collect(); - } - }); - return Some(meta); - } - None +impl Node for Metadata { + fn len(&self) -> usize { + self.to_string().len() } } #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_serialize() { @@ -143,7 +88,7 @@ mod tests { ); assert_eq!( metadata.to_string(), - "title: header\ntimestamp: 2022-01-01 00:00:00 +02:00\nimage: image\npreview: preview\ntags: tag1, tag2\n^^^\n\n" + "---\ntitle: header\ndate: 2022-01-01T00:00:00+02:00\nimage: image\npreview: preview\ntags:\n- tag1\n- tag2\n---" ); } @@ -198,7 +143,7 @@ mod tests { #[test] fn deserialize_empty() { assert_eq!( - Metadata::deserialize("^^^\n\n"), + Metadata::deserialize("---\n---"), Some(Metadata::new::<&str>(None, None, None, None, None)) ); } @@ -211,23 +156,11 @@ mod tests { #[test] fn deserialize_only_with_title() { assert_eq!( - Metadata::deserialize("title: header\n^^^\n\n"), + Metadata::deserialize("---\ntitle: header\n---"), Some(Metadata::new(Some("header"), None, None, None, None)) ); } - #[test] - fn deserialize_wrong_date() { - assert_eq!( - Metadata::deserialize("timestamp: 2022-01-01 00:00:00 +0200\n^^^\n\n"), - Some(Metadata::new::<&str>(None, None, None, None, None)) - ); - assert_eq!( - Metadata::deserialize("timestamp: 2022-01-01 00:00:00\n^^^\n\n"), - Some(Metadata::new::<&str>(None, None, None, None, None)) - ); - } - #[test] fn default() { assert_eq!( diff --git a/src/nodes/yamd.rs b/src/nodes/yamd.rs index e5b1d9b..850aa7d 100644 --- a/src/nodes/yamd.rs +++ b/src/nodes/yamd.rs @@ -163,7 +163,8 @@ impl Branch for Yamd { } fn get_outer_token_length(&self) -> usize { - 0 + let len = self.metadata.len(); + len + if len == 0 { 0 } else { 2 } } fn is_empty(&self) -> bool { @@ -174,7 +175,7 @@ impl Branch for Yamd { impl Deserializer for Yamd { fn deserialize_with_context(input: &str, _: Option) -> Option { let metadata = Metadata::deserialize(input); - let metadata_len = metadata.as_ref().map(|m| m.len()).unwrap_or(0); + let metadata_len = metadata.as_ref().map(|m| m.len() + 2).unwrap_or(0); Self::parse_branch(&input[metadata_len..], "\n\n", Self::new(metadata, vec![])) } } @@ -183,8 +184,9 @@ impl Display for Yamd { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{}{}", + "{}{}{}", self.metadata, + if self.metadata.len() == 0 { "" } else { "\n\n" }, self.nodes .iter() .map(|node| node.to_string()) @@ -201,7 +203,9 @@ impl Node for Yamd { } else { (self.nodes.len() - 1) * 2 }; - self.nodes.iter().map(|node| node.len()).sum::() + delimeter_len + self.nodes.iter().map(|node| node.len()).sum::() + + delimeter_len + + self.get_outer_token_length() } } @@ -234,12 +238,15 @@ mod tests { }; use chrono::DateTime; use pretty_assertions::assert_eq; - const TEST_CASE: &str = r#"title: test -timestamp: 2022-01-01 00:00:00 +02:00 + const TEST_CASE: &str = r#"--- +title: test +date: 2022-01-01T00:00:00+02:00 image: image preview: preview -tags: tag1, tag2 -^^^ +tags: +- tag1 +- tag2 +--- # hello