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
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 tracing::info;

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

#[must_use]
#[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_events<T>(markdown: &str, converts: T) -> Result<Vec<Event>>
where
    T: AsRef<[Convert]> + 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.to_owned(),
        });

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

    if let Err(error) = result {
        bail!("`convert_str` execution failed: {error:?}")
    } else {
        info!("Time spent on `to_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)
}