novel-cli 0.17.0

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

use color_eyre::eyre::Result;
use pulldown_cmark::{Event, MetadataBlockKind, Tag, TagEnd};

use crate::cmd::Convert;
use crate::utils::{self, Content, Metadata, Novel};

pub fn generate_pandoc_markdown<T>(novel: Novel, convert: T) -> Result<()>
where
    T: AsRef<[Convert]>,
{
    assert!(!novel.name.is_empty(), "The novel name is empty");

    let output_dir_path = utils::to_novel_dir_name(&novel.name);
    if output_dir_path.is_dir() {
        tracing::warn!("The Pandoc output directory already exists and will be deleted");
        utils::remove_file_or_dir(&output_dir_path)?;
    }

    fs::create_dir(&output_dir_path)?;
    let output_dir_path = dunce::canonicalize(output_dir_path)?;

    let markdown_file_path = output_dir_path.join(utils::to_markdown_file_name(&novel.name));

    let mut buf = String::with_capacity(2 * 1024 * 1024);

    write_metadata(&novel, &mut buf, &convert)?;
    write_introduction(&novel, &mut buf, &convert)?;
    write_chapters(&novel, &mut buf)?;

    // last \n
    if buf.ends_with("\n\n") {
        buf.pop();
    }

    fs::write(markdown_file_path, &buf)?;
    super::save_image(&novel, output_dir_path)?;

    Ok(())
}

fn write_metadata<T, C>(novel: &Novel, mut buf: T, convert: C) -> Result<()>
where
    T: fmt::Write,
    C: AsRef<[Convert]>,
{
    let description = novel
        .introduction
        .as_ref()
        .map(|introduction| introduction.join("\n"));

    let cover_image = if let Some(ref cover_image) = novel.cover_image {
        match super::cover_image_name(cover_image) {
            Ok(cover_image_name) => Some(PathBuf::from(cover_image_name)),
            Err(err) => {
                tracing::error!("Failed to get cover image name: {err}");
                None
            }
        }
    } else {
        None
    };

    let metadata = Metadata {
        title: novel.name.clone(),
        author: novel.author_name.clone(),
        lang: utils::lang(convert),
        description,
        cover_image,
    };

    let metadata_block = [
        Event::Start(Tag::MetadataBlock(MetadataBlockKind::YamlStyle)),
        Event::Text(serde_saphyr::to_string(&metadata)?.into()),
        Event::End(TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle)),
    ];

    pulldown_cmark_to_cmark::cmark(metadata_block.iter(), &mut buf)?;
    buf.write_char('\n')?;

    Ok(())
}

fn write_introduction<T, C>(novel: &Novel, mut buf: T, convert: C) -> Result<()>
where
    T: fmt::Write,
    C: AsRef<[Convert]>,
{
    if let Some(ref introduction) = novel.introduction {
        buf.write_str(&format!(
            "# {}\n\n",
            utils::convert_str("简介", convert, false)?
        ))?;

        for line in introduction {
            buf.write_str(line)?;
            buf.write_str("\n\n")?;
        }
    }

    Ok(())
}

fn write_chapters<T>(novel: &Novel, mut buf: T) -> Result<()>
where
    T: fmt::Write,
{
    let mut image_index = 1;

    for volume in &novel.volumes {
        if !volume.chapters.is_empty() {
            buf.write_str(&format!("# {}\n\n", volume.title))?;

            for chapter in &volume.chapters {
                if let Some(contents) = &chapter.contents {
                    buf.write_str(&format!("## {}\n\n", chapter.title))?;

                    for content in contents {
                        match content {
                            Content::Text(line) => {
                                buf.write_str(line)?;
                                buf.write_str("\n\n")?;
                            }
                            Content::Image(image) => {
                                match super::new_image_name(image, image_index) {
                                    Ok(image_name) => {
                                        image_index += 1;

                                        buf.write_str(&super::image_markdown_str(image_name))?;
                                        buf.write_str("\n\n")?;
                                    }
                                    Err(err) => tracing::error!("Failed to get image name: {err}"),
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(())
}