novel_cli/utils/
markdown.rs1use 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_yml::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}