1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use std::{
    fs,
    path::{Path, PathBuf},
};

use color_eyre::eyre::{bail, Result};
use pulldown_cmark::{Event, MetadataBlockKind, Options, Tag, TagEnd, TextMergeWithOffset};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

#[must_use]
#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata {
    pub title: String,
    pub author: String,
    pub lang: Lang,
    pub description: Option<String>,
    pub cover_image: Option<PathBuf>,
}

#[must_use]
#[derive(Clone, Copy, Serialize, Deserialize)]
pub enum Lang {
    #[serde(rename = "zh-Hant")]
    ZhHant,
    #[serde(rename = "zh-Hans")]
    ZhHans,
}

impl Metadata {
    pub fn cover_image_is_ok(&self) -> bool {
        !self
            .cover_image
            .as_ref()
            .is_some_and(|path| !path.is_file())
    }
}

pub fn get_metadata_from_file<T>(markdown_path: T) -> Result<Metadata>
where
    T: AsRef<Path>,
{
    let bytes = fs::read(markdown_path)?;
    let markdown = simdutf8::basic::from_utf8(&bytes)?;

    let mut parser =
        TextMergeWithOffset::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);

    get_metadata(&mut parser)
}

pub fn get_metadata(parser: &mut TextMergeWithOffset) -> Result<Metadata> {
    let event = parser.next();
    if event.is_none()
        || !matches!(
            event.unwrap().0,
            Event::Start(Tag::MetadataBlock(MetadataBlockKind::YamlStyle))
        )
    {
        bail!("Markdown files should start with a metadata block")
    }

    let metadata: Metadata;
    if let Some((Event::Text(text), _)) = parser.next() {
        metadata = serde_yaml::from_str(&text)?;
    } else {
        bail!("Metadata block content does not exist")
    }

    let event = parser.next();
    if event.is_none()
        || !matches!(
            event.unwrap().0,
            Event::End(TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle))
        )
    {
        bail!("Metadata block should end with `---` or `...`")
    }

    Ok(metadata)
}

pub fn read_markdown_to_images<T>(markdown_path: T) -> Result<Vec<PathBuf>>
where
    T: AsRef<Path>,
{
    let bytes = fs::read(markdown_path)?;
    let markdown = simdutf8::basic::from_utf8(&bytes)?;

    let mut parser =
        TextMergeWithOffset::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);

    let metadata = get_metadata(&mut parser)?;

    let parser = parser.filter_map(|(event, _)| {
        if let Event::Start(Tag::Image { dest_url, .. }) = event {
            Some(PathBuf::from(dest_url.as_ref()))
        } else {
            None
        }
    });

    let mut result: Vec<PathBuf> = parser.collect();
    if metadata.cover_image.is_some() {
        result.push(metadata.cover_image.unwrap())
    }

    Ok(result)
}