use serde::{Deserialize, Serialize};
use crate::anchor::Anchor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PageStatus {
Draft,
Canonical,
Retired,
}
impl PageStatus {
pub fn parse(s: &str) -> Option<Self> {
match s {
"draft" => Some(Self::Draft),
"canonical" => Some(Self::Canonical),
"retired" => Some(Self::Retired),
_ => None,
}
}
}
pub fn valid_slug(slug: &str) -> bool {
let segs: Vec<&str> = slug.split('/').collect();
slug.len() <= 128
&& segs.len() <= 8
&& segs.iter().all(|s| {
!s.is_empty()
&& !s.starts_with('-')
&& !s.ends_with('-')
&& !s.contains("--")
&& s.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Section {
pub order: u32,
pub text: String,
}
impl Section {
pub fn heading(&self) -> Option<&str> {
let first = self.text.lines().next()?;
let hashes = first.bytes().take_while(|b| *b == b'#').count();
if (2..=6).contains(&hashes) && first.as_bytes().get(hashes) == Some(&b' ') {
Some(first[hashes + 1..].trim())
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Note {
pub body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub covers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Page {
pub slug: String,
pub title: String,
pub kind: String,
pub status: PageStatus,
pub sections: Vec<Section>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub anchors: Vec<Anchor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<Note>,
}
impl Page {
pub fn body(&self) -> String {
crate::sections::join(&self.title, &self.sections)
}
}