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