fml 0.6.8

Friendly Markup Language
Documentation
use crate::{
    ast::*,
    parser::{
        Stream,
        functions::{macros::*, styles::*},
    },
};
use winnow::{
    Result,
    ascii::*,
    combinator::{repeat, *},
    error::{ContextError, ParserError, StrContext},
    prelude::*,
    token::*,
};

pub fn delimiteds<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    alt((
        code_inline,
        bold,
        italic,
        strikethrough,
        underlined,
        typst,
        content_warning,
        rich_text,
    ))
    .parse_next(input)
}

macro_rules! delim {
    ($token:expr, $res:expr, $input:expr) => {{
        let scope: String = delimited_esc!($token).parse_next($input)?;
        inner_fml(&scope, $input.state.clone()).map($res)
    }};
}

fn bold<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    delim!("*", FmlValue::Bold, input)
}
fn italic<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    delim!("%", FmlValue::Italic, input)
}
fn strikethrough<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    delim!("~", FmlValue::Strikethrough, input)
}
fn underlined<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    delim!("_", FmlValue::Underline, input)
}
fn typst<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    delimited_esc!("$").parse_next(input).map(FmlValue::Typst)
}
fn code_inline<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    delimited("`", enot!("`"), "`")
        .verify(|x: &str| !x.trim().is_empty())
        .parse_next(input)
        .map(FmlValue::CodeInline)
}
/// Parses and returns 3 neighbouring modifiers present in arbitrary order,
/// and returns those which were found in an ordered manner.\
/// First option matches the modifier delimited with `(` and `)`,
/// the second with `<` and `>`, and the third with `{` and `}`.
///
/// Can be told to omit matching parentheses modifier
fn parse_modifiers<'i>(
    input: &mut Stream<'i>,
    skip_paren: bool,
) -> (Option<String>, Option<String>, Option<String>) {
    let mut paren: Option<String> = None;
    let mut angled: Option<String> = None;
    let mut curly: Option<String> = None;

    // This for-loop is the best way i can think of, since this wont work due to its
    // greedy matching:
    // let (reason, fg_color, bg_color): (Option<String>, Option<String>, Option<String>) =
    //     unordered_seq!((
    //         opt(delimited_esc!("(", ")")),
    //         opt(delimited_esc!("<", ">")),
    //         opt(delimited_esc!("{", "}")),
    //     ))
    //     .parse_next(input)?;
    // also this map is the cleanest err annotation i could think of atm
    for _ in 0..3 {
        if paren.is_none() && skip_paren.not() {
            if let Ok(val) = delimited_esc!("(", ")").map_err(|()| ()).parse_next(input) {
                paren = Some(val);
            }
        }
        if angled.is_none() {
            if let Ok(val) = delimited_esc!("<", ">").map_err(|()| ()).parse_next(input) {
                angled = Some(val);
            }
        }
        if curly.is_none() {
            if let Ok(val) = delimited_esc!("{", "}").map_err(|()| ()).parse_next(input) {
                curly = Some(val);
            }
        }
    }

    (paren, angled, curly)
}
// public because of usage in scripts
pub fn content_warning<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    let content_str: String = delimited_esc!("|").parse_next(input)?;
    let vals = inner_fml(&content_str, input.state.clone())?;

    let (reason, fg_color, bg_color) = parse_modifiers(input, false);
    let reason = reason.unwrap_or("Spoilers".to_string());

    let res = FmlValue::ContentWarning { reason, body: vals };
    let res = wrap_in_colors(res, fg_color, bg_color);

    Ok(res)
}
// public because of usage in scripts
/// Must be at least one of: [Hyperlink, FgColor, BgColor]
pub fn rich_text<'i>(input: &mut Stream<'i>) -> Result<FmlValue> {
    fn rich_text_body<'i>(input: &mut Stream<'i>) -> Result<&'i str> {
        let escapes = || ('\\', alt(('[', ']', '\\')));
        let body_pt = slice_till!(1.., alt((escapes().take(), not!(escapes(), "[", "]"))));
        let body = alt((body_pt, alt((rich_text.take(), "["))));

        delimited("[", body, "]").parse_next(input)
    }
    let body = rich_text_body.parse_next(input)?;

    let in_hyperlink = input.state.is_in_hyperlink;

    let (url, fg_color, bg_color) = parse_modifiers(input, in_hyperlink);
    // Verify that at least one modifier was after the brackets, fail otherwise
    if url.is_none() && fg_color.is_none() && bg_color.is_none() {
        return fail.parse_next(input);
    }
    // **Only now** should we parse the inner fml of the body

    let vals = {
        let mut state = input.state.clone();
        state.is_in_hyperlink = url.is_some();
        inner_fml(&body, state)?
    };

    // apologies for this, if you (reader) find any cleaner way to express this, please lmk
    Ok(if let Some(uri) = url {
        let res = FmlValue::Hyperlink { fml: vals, uri };
        wrap_in_colors(res, fg_color, bg_color)
    } else {
        if let Some(color) = fg_color {
            let mut res = FmlValue::ColorFg(FmlColor { color, body: vals });
            if let Some(color) = bg_color {
                res = FmlValue::ColorBg(FmlColor {
                    color,
                    body: vec![res].into(),
                });
            }
            res
        } else {
            if let Some(color) = bg_color {
                FmlValue::ColorBg(FmlColor { color, body: vals })
            } else {
                unreachable!() // See `fail.parse_input` guard about 20~25 lines up
            }
        }
    })
}