1use serde::{Deserialize, Serialize};
6
7use crate::anchor::Anchor;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum PageStatus {
13 Draft,
15 Canonical,
17 Retired,
19}
20
21impl PageStatus {
22 pub fn parse(s: &str) -> Option<Self> {
24 match s {
25 "draft" => Some(Self::Draft),
26 "canonical" => Some(Self::Canonical),
27 "retired" => Some(Self::Retired),
28 _ => None,
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Section {
39 pub order: u32,
40 pub text: String,
41}
42
43impl Section {
44 pub fn heading(&self) -> Option<&str> {
46 let first = self.text.lines().next()?;
47 let hashes = first.bytes().take_while(|b| *b == b'#').count();
48 if (2..=6).contains(&hashes) && first.as_bytes().get(hashes) == Some(&b' ') {
49 Some(first[hashes + 1..].trim())
50 } else {
51 None
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct Note {
60 pub body: String,
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub covers: Vec<String>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct Page {
68 pub slug: String,
69 pub title: String,
70 pub kind: String,
72 pub status: PageStatus,
73 pub sections: Vec<Section>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub anchors: Vec<Anchor>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub notes: Vec<Note>,
78}
79
80impl Page {
81 pub fn body(&self) -> String {
83 crate::sections::join(&self.title, &self.sections)
84 }
85}