pretty-format 0.0.1

Rust port of pretty-format.
Documentation
mod error;
mod types;

use std::rc::Rc;

use wasm_bindgen::{JsCast, JsValue};

pub use error::PrettyFormatError;
pub use types::{Config, Plugin, PrettyFormatOptions, Printer, Refs};

use types::{Colors, Plugins};
use web_sys::js_sys::{BigInt, Number, Object};

fn print_number(val: &Number) -> String {
    if Object::is(val, &JsValue::from_f64(-0.0)) {
        "-0".into()
    } else {
        val.to_string(10)
            .expect("Number should be formatted as string.")
            .into()
    }
}

fn print_big_int(val: &BigInt) -> String {
    format!(
        "{}n",
        String::from(
            val.to_string(10)
                .expect("Number should be formatted as string.")
        )
    )
}

pub fn print_basic_value(
    val: &JsValue,
    _print_function_name: bool,
    _escape_regex: bool,
    escape_string: bool,
) -> Option<String> {
    if *val == JsValue::TRUE {
        return Some("true".into());
    }
    if *val == JsValue::FALSE {
        return Some("false".into());
    }
    if val.is_undefined() {
        return Some("undefined".into());
    }
    if val.is_null() {
        return Some("null".into());
    }

    let type_of = val.js_typeof();

    if type_of == "number" {
        return Some(print_number(val.unchecked_ref::<Number>()));
    }
    if type_of == "bigint" {
        return Some(print_big_int(val.unchecked_ref::<BigInt>()));
    }
    if type_of == "string" {
        if escape_string {
            return Some(
                val.as_string()
                    .expect("Value should be a string.")
                    .replace('"', "\\\"")
                    .replace('\\', "\\\\"),
            );
        }
        return Some(format!(
            "\"{}\"",
            val.as_string().expect("Value should be a string.")
        ));
    }

    todo!("print basic value {:?}", val)
}

pub fn print_complex_value(
    val: &JsValue,
    _config: &Config,
    _indentation: String,
    _depth: usize,
    _refs: Refs,
    _has_called_to_json: Option<bool>,
) -> String {
    todo!("print complex value {:?}", val)
}

fn print_plugin(
    plugin: Rc<dyn Plugin>,
    val: &JsValue,
    config: &Config,
    indentation: String,
    depth: usize,
    refs: Refs,
) -> String {
    plugin.serialize(val, config, indentation, depth, refs, &printer)
}

fn find_plugin(plugins: &Plugins, val: &JsValue) -> Option<Rc<dyn Plugin>> {
    plugins.iter().find(|plugin| plugin.test(val)).cloned()
}

fn printer(
    val: &JsValue,
    config: &Config,
    indentation: String,
    depth: usize,
    refs: Refs,
    has_called_to_json: Option<bool>,
) -> String {
    if let Some(plugin) = find_plugin(&config.plugins, val) {
        return print_plugin(plugin, val, config, indentation, depth, refs);
    }

    if let Some(basic_result) = print_basic_value(
        val,
        config.print_function_name,
        config.escape_regex,
        config.escape_string,
    ) {
        return basic_result;
    }

    print_complex_value(val, config, indentation, depth, refs, has_called_to_json)
}

fn validate_options(options: &PrettyFormatOptions) -> Result<(), PrettyFormatError> {
    if options.min.is_some() && options.indent.is_some_and(|indent| indent != 0) {
        Err(PrettyFormatError::Configuration(
            "Options `min` and `indent` cannot be used togther.".into(),
        ))
    } else {
        Ok(())
    }
}

fn get_colors_highlight(options: &PrettyFormatOptions) -> Colors {
    let theme = options.theme.clone().unwrap_or_default();

    Colors {
        comment: theme.comment,
        content: theme.content,
        prop: theme.prop,
        tag: theme.tag,
        value: theme.value,
    }
}

fn get_colors_empty() -> Colors {
    Colors::default()
}

fn get_print_function_name(options: &PrettyFormatOptions) -> bool {
    options.print_function_name.unwrap_or(true)
}

fn get_escape_regex(options: &PrettyFormatOptions) -> bool {
    options.escape_regex.unwrap_or(false)
}

fn get_escape_string(options: &PrettyFormatOptions) -> bool {
    options.escape_string.unwrap_or(true)
}

fn get_config(options: PrettyFormatOptions) -> Config {
    Config {
        call_to_json: options.call_to_json.unwrap_or(true),
        compare_keys: options.compare_keys.clone(),
        colors: match options.highlight {
            Some(true) => get_colors_highlight(&options),
            _ => get_colors_empty(),
        },
        escape_regex: options.escape_regex.unwrap_or(false),
        escape_string: options.escape_string.unwrap_or(true),
        indent: match options.min {
            Some(true) => "".into(),
            _ => create_indent(options.indent.unwrap_or(2)),
        },
        max_depth: options.max_depth.unwrap_or(usize::MAX),
        max_width: options.max_width.unwrap_or(usize::MAX),
        min: options.min.unwrap_or(false),
        plugins: options.plugins.unwrap_or_default(),
        print_function_name: options.print_function_name.unwrap_or(true),
        spacing_inner: match options.min {
            Some(true) => " ",
            _ => "\n",
        }
        .into(),
        spacing_outer: match options.min {
            Some(true) => "",
            _ => "\n",
        }
        .into(),
    }
}

fn create_indent(indent: usize) -> String {
    " ".repeat(indent)
}

pub fn format(val: &JsValue, options: PrettyFormatOptions) -> Result<String, PrettyFormatError> {
    validate_options(&options)?;

    if let Some(plugins) = &options.plugins {
        if let Some(plugin) = find_plugin(plugins, val) {
            return Ok(print_plugin(
                plugin,
                val,
                &get_config(options),
                "".into(),
                0,
                vec![],
            ));
        }
    }

    let basic_result = print_basic_value(
        val,
        get_print_function_name(&options),
        get_escape_regex(&options),
        get_escape_string(&options),
    );
    if let Some(basic_result) = basic_result {
        Ok(basic_result)
    } else {
        Ok(print_complex_value(
            val,
            &get_config(options),
            "".into(),
            0,
            vec![],
            None,
        ))
    }
}