ahtml-from-markdown 0.2.0

Convert Markdown to ahtml HTML element trees
Documentation
use std::{
    borrow::Borrow,
    collections::{
        btree_map,
        hash_map::{Entry, OccupiedEntry},
        BTreeMap, HashMap,
    },
    env::VarError,
    ffi::{OsStr, OsString},
    fmt::{Debug, Display},
    fs::create_dir_all,
    hash::Hash,
    path::PathBuf,
    time::Duration,
};

use anyhow::{anyhow, bail, Context, Result};
use num::CheckedAdd;

/// Return name of the enum value, without the rest of its Debug
/// serialisation. Slower than it needed to be given better ways, but
/// what would those be?
pub fn enum_name<T: Debug>(v: T) -> String {
    let mut s = format!("{v:?}");
    if let Some(i) = s.find(|c| c == '(') {
        s.shrink_to(i);
        s
    } else {
        s
    }
}

/// Return last element of a Vec, first creating it if not present.
pub fn autovivify_last<T>(v: &mut Vec<T>, create: impl FnOnce() -> T) -> &mut T {
    let v: *mut _ = v; // Work around serious rustc deficiency?
    if let Some(last) = unsafe { &mut *v }.last_mut() {
        last
    } else {
        unsafe { &mut *v }.push(create());
        unsafe { &mut *v }.last_mut().unwrap()
    }
}

// XX I have *done* these before
// fn str_skip_while(s: &str, pred: impl Fn(char) -> bool) -> &str {
//     if let Some(i) = s.find(|c| c != '/') {
//         &s[i..]
//     } else {
//         s
//     }
// }

// fn rootbased_to_relative(s: &str) -> &str {
//     str_skip_while(s, |c| c != '/')
// }

// A HashMap::get_mut variant that allows to work around the issue
// that rustc (pre polonius) does not let go of the reference in the
// None case; so we use an Err instead and pick up the reference from
// there.
pub fn hashmap_get_mut<'m, K: Eq + Hash, P: Eq + Hash + ?Sized, V>(
    m: &'m mut HashMap<K, V>,
    k: &P,
) -> Result<&'m mut V, &'m mut HashMap<K, V>>
where
    K: Borrow<P>,
{
    let pm: *mut _ = m;
    // Safe because in the true branch we track using the lifetimes
    // (just like in the original get_mut), and in the false branch we
    // just pass the input value.
    if let Some(v) = unsafe { &mut *pm }.get_mut(k) {
        Ok(v)
    } else {
        Err(unsafe { &mut *pm })
    }
}

// Same (see hashmap_get_mut) for BTreeMap.
pub fn btreemap_get_mut<'m, K: Ord, P: Ord + ?Sized, V>(
    m: &'m mut BTreeMap<K, V>,
    k: &P,
) -> Result<&'m mut V, &'m mut BTreeMap<K, V>>
where
    K: Borrow<P>,
{
    let pm: *mut _ = m;
    // Safe because in the true branch we track using the lifetimes
    // (just like in the original get_mut), and in the false branch we
    // just pass the input value.
    if let Some(v) = unsafe { &mut *pm }.get_mut(k) {
        Ok(v)
    } else {
        Err(unsafe { &mut *pm })
    }
}

// Modified copy of #[unstable(feature = "map_try_insert", issue =
// "82766")] from
// https://doc.rust-lang.org/src/std/collections/hash/map.rs.html#1132-1137,
// avoiding OccupiedError because that's also unstable. FUTURE:
// replace with try_insert.
pub fn hashmap_try_insert<'m, K: Eq + Hash, V>(
    m: &'m mut HashMap<K, V>,
    key: K,
    value: V,
) -> Result<&mut V, OccupiedEntry<K, V>> {
    match m.entry(key) {
        Entry::Occupied(entry) => Err(entry),
        Entry::Vacant(entry) => Ok(entry.insert(value)),
    }
}

// Modified copy of #[unstable(feature = "map_try_insert", issue =
// "82766")] from
// https://doc.rust-lang.org/src/alloc/collections/btree/map.rs.html#1016-1018;
// see comments on hashmap_try_insert.
pub fn btreemap_try_insert<'m, K: Ord, V>(
    m: &'m mut BTreeMap<K, V>,
    key: K,
    value: V,
) -> Result<&'m mut V, btree_map::OccupiedEntry<K, V>> {
    match m.entry(key) {
        btree_map::Entry::Occupied(entry) => Err(entry),
        btree_map::Entry::Vacant(entry) => Ok(entry.insert(value)),
    }
}

/// Similar to `?` in a context that returns `Option`, this propagates
/// `None` values, but wraps them in `Ok`. I.e. behaves like `?`
/// except if the `Option` context is wrapped in a `Result`.
#[macro_export]
macro_rules! or_return_none {
    ($e:expr) => {{
        let res = $e;
        if let Some(val) = res {
            val
        } else {
            return Ok(None);
        }
    }};
}

// Sigh, for exponential backoff, everybody doing this for themselves?
pub fn duration_mul_div(orig: Duration, multiplier: u64, divider: u64) -> Option<Duration> {
    let nanos: u64 = orig
        .as_nanos()
        .checked_mul(multiplier as u128)?
        .checked_div(divider as u128)?
        .try_into()
        .ok()?;
    Some(Duration::from_nanos(nanos))
}

pub fn debug_stringlikes<S: Display>(v: &[S]) -> Vec<String> {
    v.iter().map(|s| s.to_string()).collect()
}

/// A loop that caches errors and retries with exponential
/// backoff. (Backoff parameters and error messaging hard coded for
/// now, as is anyhow::Result.)
#[macro_export]
macro_rules! loop_try {
    ( $($body_parts:tt)* ) => {{
        let default_error_sleep_duration = Duration::from_millis(500);
        let mut error_sleep_duration = default_error_sleep_duration;
        loop {
            match (|| -> Result<()> { $($body_parts)* })() {
                Ok(()) => {
                    error_sleep_duration = default_error_sleep_duration;
                }
                Err(e) => {
                    eprintln!("loop_try: got error {e:#}, sleeping for \
                               {error_sleep_duration:?}");
                    thread::sleep(error_sleep_duration);
                    error_sleep_duration =
                        $crate::util::duration_mul_div(error_sleep_duration,
                                         1200,
                                         1000)
                        .unwrap_or(default_error_sleep_duration);
                }
            }
        }
    }}
}

#[macro_export]
macro_rules! try_do {
    ( $($b:tt)* ) => ( (|| { $($b)* })() )
}

#[macro_export]
macro_rules! try_result {
    ( $($b:tt)* ) => ( (|| -> Result<_, _> { $($b)* })() )
}

#[macro_export]
macro_rules! try_option {
    ( $($b:tt)* ) => ( (|| -> Option<_> { $($b)* })() )
}

/// A counter. Panics if T wraps around.
pub fn infinite_sequence<T: CheckedAdd + Copy>(start: T, inc: T) -> impl FnMut() -> T {
    let mut current = start;
    move || -> T {
        let n = current;
        current = n.checked_add(&inc).expect("number not overflowing");
        n
    }
}

pub fn alphanumber(i: u32) -> String {
    let mut s = Vec::new();
    let mut j: i64 = i.into();
    while j >= 0 {
        s.push(b'a' + ((j % 26) as u8));
        j = (j / 26) - 1;
    }
    s.reverse();
    String::from_utf8(s).expect("all ascii")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn t_alphanumber() {
        fn t(i: u32, s: &str) {
            assert_eq!(&alphanumber(i), s);
        }
        t(0, "a");
        t(1, "b");
        t(26, "aa");
        t(27, "ab");
        t(27 * 26 - 1, "zz");
        t(27 * 26, "aaa");
        t(27 * 26 + 1, "aab");
        t(27 * 26 + 26, "aba");
    }
}

pub fn osstring_into_string(s: OsString) -> Result<String> {
    match s.into_string() {
        Ok(s2) => Ok(s2),
        Err(s) => bail!("can't properly decode to string {:?}", s.to_string_lossy()),
    }
}

pub fn osstr_to_str(s: &OsStr) -> Result<&str> {
    match s.to_str() {
        Some(s2) => Ok(s2),
        None => bail!("can't properly decode to string {:?}", s.to_string_lossy()),
    }
}

pub fn program_path() -> Result<String> {
    let path = std::env::args_os()
        .into_iter()
        .next()
        .ok_or_else(|| anyhow!("missing program executable path in args_os"))?;
    osstring_into_string(path)
        .with_context(|| anyhow!("decoding of program executable path failed"))
}

pub fn program_name() -> Result<String> {
    let path = std::env::args_os()
        .into_iter()
        .next()
        .ok_or_else(|| anyhow!("missing program executable path in args_os"))?;
    let pb = PathBuf::from(path);
    let fname = pb
        .file_name()
        .ok_or_else(|| anyhow!("cannot get file name from path {:?}", pb.to_string_lossy()))?;
    Ok(osstr_to_str(fname)
        .with_context(|| anyhow!("cannot decode file name {:?}", fname.to_string_lossy()))?
        .to_string())
}

pub fn log_basedir() -> Result<String> {
    let logbasedir = format!(
        "{}/log/{}",
        std::env::var("HOME").with_context(|| anyhow!("can't get HOME env var"))?,
        program_name()?
    );
    // XX todo: perms / umask!
    create_dir_all(&logbasedir)
        .with_context(|| anyhow!("can't create log base directory {:?}", logbasedir))?;
    Ok(logbasedir)
}

/// Get an env var as a String; decoding failures are reported as
/// errors. If the var is not set and no fallback was given, an error
/// is reported as well.
pub fn getenv_or(name: &str, fallbackvalue: Option<&str>) -> Result<String> {
    match std::env::var(name) {
        Ok(s) => Ok(s),
        Err(e) => match e {
            VarError::NotPresent => match fallbackvalue {
                Some(v) => Ok(v.to_string()),
                None => bail!(
                    "{name:?} env var is missing and \
                                   no default provided"
                ),
            },
            VarError::NotUnicode(_) => bail!("{name:?} env var is not unicode"),
        },
    }
}

/// Get an env var as a String; decoding failures are reported as
/// errors.
pub fn getenv(name: &str) -> Result<Option<String>> {
    match std::env::var(name) {
        Ok(s) => Ok(Some(s)),
        Err(e) => match e {
            VarError::NotPresent => Ok(None),
            VarError::NotUnicode(_) => bail!("{name:?} env var is not unicode"),
        },
    }
}

/// Like getenv but reports an error mentioning the variable name if
/// it isn't set.
pub fn xgetenv(name: &str) -> Result<String> {
    getenv(name)?.ok_or_else(|| anyhow!("missing env var {name:?}"))
}

/// Retrieve a boolean from an env var. A missing env var or the
/// strings "0", "false" or "no" represent false, a present env var
/// with the strings "", "1", "true", or "yes" represent true.
pub fn getenv_bool(name: &str) -> Result<bool> {
    if let Some(s) = getenv(name)? {
        match &*s {
            "" => Ok(true), // really?
            "1" => Ok(true),
            "true" => Ok(true),
            "yes" => Ok(true),
            "0" => Ok(false),
            "false" => Ok(false),
            "no" => Ok(false),
            _ => bail!("boolean env var {name:?} has invalid contents {s:?}"),
        }
    } else {
        Ok(false)
    }
}

/// Takes a place (variable or field) holding an `Option<T>` and an
/// expression that returns `T`; returns a `&T` to the value held by
/// the `Option`, runs the expression and stores the result in the
/// place if it holds `None`. The name is from what the `//` operator
/// in Perl (there`s also `//=` which this is really) is called. The
/// expression is executed in the context of the `oerr` call, with no
/// additional subroutine wrapper, meaning e.g. `?` jumps out of the
/// `oerr` (it is designed like this on purpose).
#[macro_export]
macro_rules! oerr {
    { $var:expr, $e:expr } => {
        if let Some(v) = &mut $var {
            v
        } else {
            $var = Some($e);
            $var.as_mut().unwrap()
        }
    }
}