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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use std::{
    fs,
    panic::{self, RefUnwindSafe},
    path::{Path, PathBuf},
};

use color_eyre::eyre::{bail, ensure, Result};
use novel_api::Timing;
use parking_lot::Mutex;
use pulldown_cmark::{Event, Options, Parser, Tag};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use tracing::info;

use crate::{
    cmd::Convert,
    utils::{self, LINE_BREAK},
};

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

impl MetaData {
    pub fn lang_is_ok(&self) -> bool {
        self.lang == "zh-Hant" || self.lang == "zh-Hans"
    }

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

pub fn read_markdown<T>(markdown_path: T) -> Result<(MetaData, String)>
where
    T: AsRef<Path>,
{
    let mut timing = Timing::new();

    let markdown_path = markdown_path.as_ref();

    let bytes = fs::read(markdown_path)?;
    let markdown = simdutf8::basic::from_utf8(&bytes)?;
    utils::verify_line_break(markdown)?;

    ensure!(
        markdown.starts_with("---"),
        "The markdown format is incorrect, it should start with `---`"
    );

    if let Some(index) = markdown.find(format!("{0}...{0}", LINE_BREAK).as_str()) {
        let yaml = &markdown[3 + LINE_BREAK.len()..index];

        let meta_data: MetaData = serde_yaml::from_str(yaml)?;
        let markdown = markdown[index + 3 + LINE_BREAK.len() * 2..].to_string();

        info!("Time spent on `read_markdown`: {}", timing.elapsed()?);

        Ok((meta_data, markdown))
    } else {
        bail!("The markdown format is incorrect, it should end with `...`");
    }
}

pub fn to_markdown_events<T, E>(
    markdown: &str,
    converts: T,
    input_dir: E,
    delete: bool,
) -> Result<Vec<Event>>
where
    T: AsRef<[Convert]> + Sync + RefUnwindSafe,
    E: AsRef<Path> + Sync + RefUnwindSafe,
{
    let mut timing = Timing::new();

    let parser = Parser::new_ext(markdown, Options::empty());
    let events = parser.collect::<Vec<_>>();

    let result = panic::catch_unwind(|| {
        let iter = events.into_par_iter().map(|event| match event {
            Event::Text(text) => {
                Event::Text(utils::convert_str(text, converts.as_ref()).unwrap().into())
            }
            Event::Start(Tag::Image(link_type, path, title)) => {
                let new_image_path =
                    super::convert_image(input_dir.as_ref().join(path.into_string()), delete)
                        .unwrap();

                Event::Start(Tag::Image(
                    link_type,
                    new_image_path
                        .file_name()
                        .unwrap()
                        .to_str()
                        .unwrap()
                        .to_string()
                        .into(),
                    title,
                ))
            }
            _ => event.to_owned(),
        });

        iter.collect::<Vec<Event>>()
    });

    if let Err(error) = result {
        bail!("{error:?}")
    } else {
        info!("Time spent on `to_markdown_events`: {}", timing.elapsed()?);

        Ok(result.unwrap())
    }
}

pub fn read_markdown_to_images<T>(markdown_path: T) -> Result<Vec<PathBuf>>
where
    T: AsRef<Path>,
{
    let (metadata, markdown) = read_markdown(markdown_path)?;

    let parser = Parser::new_ext(&markdown, Options::empty());
    let events = parser.collect::<Vec<_>>();

    let result = Mutex::new(Vec::new());

    events.into_par_iter().for_each(|event| {
        if let Event::Start(Tag::Image(_, path, _)) = event {
            result.lock().push(PathBuf::from(&path.to_string()));
        }
    });

    let mut result = result.lock().to_vec();
    if metadata.cover_image.is_some() {
        result.push(metadata.cover_image.unwrap())
    }

    Ok(result)
}