news-flash 3.0.1

Base library for a modern feed reader
Documentation
pub mod favicons;
pub mod feed_parser;
pub mod greader;
pub mod html2text;
pub mod opml;
pub mod relative_url_evaluater;
pub mod text2html;
pub mod thumbnail;

use chrono::{DateTime, TimeZone, Utc};
use std::path::Path;

// Two goals here:
//   1) append an * after every term so it becomes a prefix search
//      (see <https://www.sqlite.org/fts3.html#section_3>), and
//   2) strip out common words/operators that might get interpreted as
//      search operators.
// We ignore everything inside quotes to give the user a way to
// override our algorithm here.  The idea is to offer one search query
// syntax for Geary that we can use locally and via IMAP, etc.
pub fn prepare_search_term(search_term: &str) -> String {
    let mut search_term_balanced = search_term.replace('\'', " ");

    // Remove the last quote if it's not balanced.  This has the
    // benefit of showing decent results as you type a quoted phrase.
    if !count_char(search_term, &'"').is_multiple_of(2)
        && let Some(last_quote) = search_term.rfind('"')
    {
        search_term_balanced.replace_range(last_quote..last_quote + 1, " ");
    }

    let mut in_quote = false;
    let mut prepared_search_term = String::new();
    for word in search_term_balanced.split_whitespace() {
        let mut quotes = count_char(word, &'"');
        let mut word = word.to_owned();

        if !in_quote && quotes > 0 {
            in_quote = true;
            quotes -= 1;
        }

        if !in_quote {
            let lower = word.to_lowercase();
            if lower == "and" || lower == "or" || lower == "not" || lower == "near" {
                continue;
            }

            if word.starts_with('-') {
                word.remove(0);
            }

            if word.is_empty() {
                continue;
            }

            word = format!("\"{word}*\"");
        }

        if in_quote && !quotes.is_multiple_of(2) {
            in_quote = false;
        }

        prepared_search_term.push_str(&word);
        prepared_search_term.push(' ');
    }

    prepared_search_term
}

pub fn count_char(string: &str, character: &char) -> usize {
    string.chars().filter(|c| c == character).count()
}

pub fn vec_to_option<T>(vector: Vec<T>) -> Option<Vec<T>> {
    if vector.is_empty() { None } else { Some(vector) }
}

pub fn option_to_bool(option: Option<bool>) -> bool {
    option.unwrap_or(false)
}

pub fn file_size(path: &Path) -> Result<u64, std::io::Error> {
    let meta_data = std::fs::metadata(path)?;
    if !meta_data.is_file() {
        return Err(std::io::ErrorKind::InvalidInput.into());
    }

    Ok(meta_data.len())
}

pub fn folder_size(path: &Path) -> Result<u64, std::io::Error> {
    let mut dir_size = 0;

    let dir = std::fs::read_dir(path)?;

    for file in dir {
        let file = file?;
        let metadata = file.metadata()?;
        let size = match metadata {
            metadata if metadata.is_dir() => folder_size(&file.path())?,
            metadata => metadata.len(),
        };

        dir_size += size;
    }

    Ok(dir_size)
}

pub fn timestamp_to_datetime(timestamp: i64) -> DateTime<Utc> {
    Utc.timestamp_opt(timestamp, 0).single().unwrap_or_else(Utc::now)
}