novel_cli/utils/
markdown.rs

1use std::fs;
2use std::ops::Range;
3use std::path::{Path, PathBuf};
4
5use color_eyre::eyre::{self, Result};
6use pulldown_cmark::{Event, MetadataBlockKind, Options, Parser, Tag, TagEnd, TextMergeWithOffset};
7use serde::{Deserialize, Serialize};
8use serde_with::skip_serializing_none;
9
10#[must_use]
11#[skip_serializing_none]
12#[derive(Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub struct Metadata {
15    pub title: String,
16    pub author: String,
17    pub lang: Lang,
18    pub description: Option<String>,
19    pub cover_image: Option<PathBuf>,
20}
21
22#[must_use]
23#[derive(Clone, Copy, Serialize, Deserialize)]
24pub enum Lang {
25    #[serde(rename = "zh-Hant")]
26    ZhHant,
27    #[serde(rename = "zh-Hans")]
28    ZhHans,
29}
30
31impl Metadata {
32    pub fn cover_image_is_ok(&self) -> bool {
33        self.cover_image.as_ref().is_none_or(|path| path.is_file())
34    }
35}
36
37pub fn get_metadata_from_file<T>(markdown_path: T) -> Result<Metadata>
38where
39    T: AsRef<Path>,
40{
41    let bytes = fs::read(markdown_path)?;
42    let markdown = simdutf8::basic::from_utf8(&bytes)?;
43
44    let mut parser = TextMergeWithOffset::new(
45        Parser::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS).into_offset_iter(),
46    );
47
48    get_metadata(&mut parser)
49}
50
51pub fn get_metadata<'a, T>(parser: &mut TextMergeWithOffset<'a, T>) -> Result<Metadata>
52where
53    T: Iterator<Item = (Event<'a>, Range<usize>)>,
54{
55    let event = parser.next();
56    if event.is_none()
57        || !matches!(
58            event.unwrap().0,
59            Event::Start(Tag::MetadataBlock(MetadataBlockKind::YamlStyle))
60        )
61    {
62        eyre::bail!("Markdown files should start with a metadata block")
63    }
64
65    let metadata: Metadata;
66    if let Some((Event::Text(text), _)) = parser.next() {
67        metadata = serde_yaml::from_str(&text)?;
68    } else {
69        eyre::bail!("Metadata block content does not exist")
70    }
71
72    let event = parser.next();
73    if event.is_none()
74        || !matches!(
75            event.unwrap().0,
76            Event::End(TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle))
77        )
78    {
79        eyre::bail!("Metadata block should end with `---` or `...`")
80    }
81
82    Ok(metadata)
83}
84
85pub fn read_markdown_to_images<T>(markdown_path: T) -> Result<Vec<PathBuf>>
86where
87    T: AsRef<Path>,
88{
89    let bytes = fs::read(markdown_path)?;
90    let markdown = simdutf8::basic::from_utf8(&bytes)?;
91
92    let mut parser = TextMergeWithOffset::new(
93        Parser::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS).into_offset_iter(),
94    );
95
96    let metadata = get_metadata(&mut parser)?;
97
98    let parser = parser.filter_map(|(event, _)| {
99        if let Event::Start(Tag::Image { dest_url, .. }) = event {
100            Some(PathBuf::from(dest_url.as_ref()))
101        } else {
102            None
103        }
104    });
105
106    let mut result: Vec<PathBuf> = parser.collect();
107    if metadata.cover_image.is_some() {
108        result.push(metadata.cover_image.unwrap())
109    }
110
111    Ok(result)
112}