novel-cli 0.17.0

A set of tools for downloading novels from the web, manipulating text, and generating EPUB
Documentation
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};

use color_eyre::eyre::{self, Result};
use pulldown_cmark::{Event, MetadataBlockKind, Options, Parser, Tag, TagEnd, TextMergeWithOffset};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

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

#[must_use]
#[derive(Clone, Copy, Serialize, Deserialize)]
pub enum Lang {
    #[serde(rename = "zh-Hant")]
    ZhHant,
    #[serde(rename = "zh-Hans")]
    ZhHans,
}

impl Metadata {
    pub fn cover_image_is_ok(&self) -> bool {
        self.cover_image.as_ref().is_none_or(|path| path.is_file())
    }
}

pub fn get_metadata_from_file<T>(markdown_path: T) -> Result<Metadata>
where
    T: AsRef<Path>,
{
    let bytes = fs::read(markdown_path)?;
    let markdown = simdutf8::basic::from_utf8(&bytes)?;

    let mut parser = TextMergeWithOffset::new(
        Parser::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS).into_offset_iter(),
    );

    get_metadata(&mut parser)
}

pub fn get_metadata<'a, T>(parser: &mut TextMergeWithOffset<'a, T>) -> Result<Metadata>
where
    T: Iterator<Item = (Event<'a>, Range<usize>)>,
{
    let event = parser.next();
    if event.is_none()
        || !matches!(
            event.unwrap().0,
            Event::Start(Tag::MetadataBlock(MetadataBlockKind::YamlStyle))
        )
    {
        eyre::bail!("Markdown files should start with a metadata block")
    }

    let metadata: Metadata;
    if let Some((Event::Text(text), _)) = parser.next() {
        metadata = serde_saphyr::from_str(&text)?;
    } else {
        eyre::bail!("Metadata block content does not exist")
    }

    let event = parser.next();
    if event.is_none()
        || !matches!(
            event.unwrap().0,
            Event::End(TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle))
        )
    {
        eyre::bail!("Metadata block should end with `---` or `...`")
    }

    Ok(metadata)
}

pub fn read_markdown_to_images<T>(markdown_path: T) -> Result<Vec<PathBuf>>
where
    T: AsRef<Path>,
{
    let bytes = fs::read(markdown_path)?;
    let markdown = simdutf8::basic::from_utf8(&bytes)?;

    let mut parser = TextMergeWithOffset::new(
        Parser::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS).into_offset_iter(),
    );

    let metadata = get_metadata(&mut parser)?;

    let parser = parser.filter_map(|(event, _)| {
        if let Event::Start(Tag::Image { dest_url, .. }) = event {
            Some(PathBuf::from(dest_url.as_ref()))
        } else {
            None
        }
    });

    let mut result: Vec<PathBuf> = parser.collect();
    if let Some(cover_image) = metadata.cover_image {
        result.push(cover_image)
    }

    Ok(result)
}