Skip to content

Commit

Permalink
YAML metadata (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lurk authored Nov 2, 2023
1 parent 6d557fe commit b546c9c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 121 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
19 changes: 11 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//! ---
//!
//! ```
//!
Expand Down
143 changes: 38 additions & 105 deletions src/nodes/metadata.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub timestamp: Option<DateTime<FixedOffset>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<DateTime<FixedOffset>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}

impl Metadata {
Expand All @@ -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<Self> {
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::<usize>()
+ 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<Context>) -> Option<Self> {
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() {
Expand All @@ -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---"
);
}

Expand Down Expand Up @@ -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))
);
}
Expand All @@ -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!(
Expand Down
23 changes: 15 additions & 8 deletions src/nodes/yamd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ impl Branch<YamdNodes> 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 {
Expand All @@ -174,7 +175,7 @@ impl Branch<YamdNodes> for Yamd {
impl Deserializer for Yamd {
fn deserialize_with_context(input: &str, _: Option<Context>) -> Option<Self> {
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![]))
}
}
Expand All @@ -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())
Expand All @@ -201,7 +203,9 @@ impl Node for Yamd {
} else {
(self.nodes.len() - 1) * 2
};
self.nodes.iter().map(|node| node.len()).sum::<usize>() + delimeter_len
self.nodes.iter().map(|node| node.len()).sum::<usize>()
+ delimeter_len
+ self.get_outer_token_length()
}
}

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit b546c9c

Please sign in to comment.