fml 0.3.4

Friendly Markup Language
Documentation
use chumsky::{
	IterParser, Parser,
	combinator::{IgnoreThen, Not, Rewind},
	container::Seq,
	error::{RichReason, Simple},
	extra::{self, Err, Full},
	input::{Input, Stream},
	primitive::{Any, Choice, any, choice, empty, end, just, none_of, one_of, todo},
	recursive::{Recursive, recursive},
	regex::regex,
	span::SimpleSpan,
	text::{ascii::ident, inline_whitespace, newline, unicode, whitespace},
};

use crate::{
	ast::{
		Content, FmlCodeBlockValue, FmlFunction, FmlValue, FunctionToken, Section,
		Token::{self, CodeInline},
		TokenVec,
	},
	lls,
	parser::FML_TOKENS,
};

// Trying my darnest to improve compile times by throwing in type declarations anywhere sensible

/// Progressing escape, consuming both the backslash and the token after it.
pub fn esc<'a>() -> impl Parser<'a, &'a str, char, extra::Err<Simple<'a, char>>> + Copy {
	just('\\').ignore_then(one_of(FML_TOKENS))
}

/// Nonprogressing escape, used for consuming the backslash and keeping the cursor at the char after it.
pub fn esc_np<'a>() -> impl Parser<'a, &'a str, char, extra::Err<Simple<'a, char>>> + Copy {
	just('\\').ignore_then(one_of(FML_TOKENS).rewind())
}

/// Progressing escape of one newline token.
pub fn esc_nl<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Simple<'a, char>>> + Copy {
	just('\\').ignore_then(newline())
}

/// Nonprogressing escape of one newline token,
/// consumes the backslash and keeps the cursor at the newline token.
pub fn esc_nl_np<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Simple<'a, char>>> + Copy {
	just('\\').ignore_then(newline().rewind())
}

/// Detects Start-of-line caused via either the beginning of the file or newline tokens.
pub fn sol<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Simple<'a, char>>> + Clone {
	choice((
		regex(r"^").ignored(),
		empty().delimited_by(newline(), empty()),
	))
}

pub fn nl<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Simple<'a, char>>> + Clone {
	choice((newline(), esc_nl()))
}

pub fn parser<'a>() -> impl Parser<'a, &'a str, Content, extra::Err<Simple<'a, char>>> {
	let section_sep = choice((
		inline_whitespace()
			.ignore_then(newline())
			.repeated()
			.at_least(2),
		whitespace().ignore_then(end()),
	));

	let section = any()
		.and_is(section_sep.not())
		.repeated()
		.at_least(1)
		.to_slice();

	let section = fml_values()
		.try_map(|x, _| {
			TokenVec(x)
				.try_into()
				.map_err(|()| Simple::new(None, SimpleSpan::default()))
		})
		.nested_in(section)
		.map(Section);

	section.padded().repeated().collect().map(Content)
}

pub fn fml_values<'a>() -> impl Parser<'a, &'a str, Vec<Token>, extra::Err<Simple<'a, char>>> + Clone
{
	fml_value().repeated().at_least(1).collect()
}

pub fn fml_value<'a>() -> impl Parser<'a, &'a str, Token, extra::Err<Simple<'a, char>>> + Clone {
	recursive(|fml| {
		let fml = || fml.clone();

		let mut delimiteds = Recursive::declare();
		let mut nestables = Recursive::declare();
		let mut headings = Recursive::declare();

		delimiteds.define({
			let inner = fml().repeated().at_least(1).collect::<Vec<Token>>();
			let inner = || inner.clone();

			let spoiler = inner().nested_in(delimited_slice!(just("|")));
			let bold = inner().nested_in(delimited_slice!(just("*")));
			let underlined = inner().nested_in(delimited_slice!(just("__")));
			let italic = inner().nested_in(delimited_slice!(just("_")));
			let strikethrough = inner().nested_in(delimited_slice!(just("~")));
			let typst = delimited_string!(just("$"));

			choice((
				spoiler.map(Token::CwSpoiler),
				bold.map(Token::Bold),
				underlined.map(Token::Underline),
				italic.map(Token::Italic),
				strikethrough.map(Token::Strikethrough),
				typst.map(Token::Typst),
			))
		});

		let line_longs = choice((nestables.clone(), headings.clone()));

		nestables.define({
			let quo_start = just('>').ignore_then(inline_whitespace().at_least(1));
			// Quotes must be postprocessed
			let quote = sol().ignore_then(quo_start).ignore_then(
				// Now we're actually in the quote and gotta ensure it repeats on
				// "\n>\s" and "\\\n" and breaks if its just "\n"
				choice((
					just('\\').ignore_then(any().and_is(newline())),
					any().and_is(newline()).then_ignore(quo_start),
					any().and_is(newline().not()),
				))
				.repeated()
				.at_least(1)
				.collect::<String>()
				.map(Token::Quote),
			);

			let list_inner = fml()
				.repeated()
				.at_least(1)
				.collect::<Vec<Token>>()
				.map(Token::ListItem);

			choice((quote, list_inner.nested_in(lls!(just('-')))))
				// Removes newline token that appears between the end of a line-long token and a
				// non-line-long token. This is removed because headings and nestables take
				// up an entire line by definition.
				.then_ignore(line_longs.clone().not().then(newline()).ignored().or_not())
		});

		// Defining the loosely related styles below as close to the Headings section as possible
		// So that they can be included in its custom `inner` and `plain` definitions.

		let code_styles = choice((code_block(), code_inline()));

		let function_body = fml()
			.repeated()
			.at_least(1)
			.collect::<Vec<Token>>()
			.nested_in(function_body_pat());
		let function = just('#')
			.ignore_then(ident())
			.then(function_params())
			.then(function_body.clone())
			.map(
				|((name, params), body): ((&str, Option<String>), Vec<Token>)| {
					Token::Function(FunctionToken {
						name: name.to_string(),
						params,
						body,
					})
				},
			);

		let sup_shorthand = just("^")
			.ignore_then(function_body.clone())
			.map(Token::Superscript);
		let sub_shorthand = just("_").ignore_then(function_body).map(Token::Subscript);
		let functions = choice((function, sup_shorthand, sub_shorthand));

		let hyperlink_fml = fml()
			.repeated()
			.at_least(1)
			.collect::<Vec<Token>>()
			.nested_in(delimited_slice!(just("["), just("]")));
		let hyperlink_uri = delimited_string!(just("("), just(")"));

		let hyperlink = hyperlink_fml
			.then(hyperlink_uri)
			.map(|(fml, uri)| Token::Hyperlink { fml, uri });

		headings.define({
			// Headings define a custom `inner` and plain text that prevents headings inside of headings.
			let inner = choice((
				code_styles.clone(),
				functions.clone(),
				hyperlink.clone(),
				delimiteds.clone(),
				nestables.clone(),
				plain(choice((
					code_styles.clone(),
					functions.clone(),
					hyperlink.clone(),
					delimiteds.clone(),
					nestables.clone(),
					nl().to(Token::Linebreak),
				))),
			))
			.repeated()
			.at_least(1)
			.collect::<Vec<Token>>();
			let inner = || inner.clone();

			let head_1 = inner().nested_in(lls!(just('#')));
			let head_2 = inner().nested_in(lls!(just("##")));
			let head_3 = inner().nested_in(lls!(just("###")));
			let sup_line = inner().nested_in(lls!(just("#^")));
			let sub_line = inner().nested_in(lls!(just("#!"), just("#_")));
			choice((
				head_1.map(Token::Heading1),
				head_2.map(Token::Heading2),
				head_3.map(Token::Heading3),
				sup_line.map(Token::SupLine),
				sub_line.map(Token::SubLine),
			))
			// Removes newline token that appears between the end of a line-long token and a
			// non-line-long token. This is removed because headings and nestables take
			// up an entire line by definition.
			.then_ignore(line_longs.not().then(newline()).ignored().or_not())
		});

		choice((
			code_styles.clone(),
			functions.clone(),
			hyperlink.clone(),
			headings.clone(),
			nestables.clone(),
			delimiteds.clone(),
			// Newline must be attempted only *after* line-long patterns
			nl().to(Token::Linebreak),
			plain(choice((
				code_styles,
				functions,
				hyperlink,
				delimiteds,
				headings,
				nestables,
				nl().to(Token::Linebreak),
			))),
		))
	})
}

pub fn plain<'a>(
	break_on: impl Parser<'a, &'a str, Token, Err<Simple<'a, char>>> + Clone,
) -> impl Parser<'a, &'a str, Token, extra::Err<Simple<'a, char>>> + Clone {
	let non_style = choice((esc(), any().and_is(break_on.not())));

	choice((esc(), any()))
		.then(non_style.repeated().collect::<String>())
		.map(|(ch, mut s): (char, String)| {
			s.insert(0, ch); // append first parsed char to str
			Token::Text(s)
		})
}

pub fn code_inline<'a>() -> impl Parser<'a, &'a str, Token, Err<Simple<'a, char>>> + Clone {
	// Not `delimited_string!` because of additional no-newline requirement
	// ,,, though it could be with a proper $body
	choice((
		choice((just(r"\`").ignored(), just("`").ignored(), newline()))
			.not()
			.rewind(),
		just("\\").ignore_then(just("`").rewind()).ignored(),
	))
	.ignore_then(any())
	.repeated()
	.at_least(1)
	.collect::<String>()
	.delimited_by(just('`'), just('`'))
	.map(CodeInline)
}

pub fn code_block<'a>() -> impl Parser<'a, &'a str, Token, Err<Simple<'a, char>>> + Clone {
	let linebreak = inline_whitespace().ignore_then(newline());

	let body_str = choice((
		any()
			.and_is(
				choice((
					linebreak,
					just(r"\```").ignored(),
					whitespace().then(just("```")).ignored(),
				))
				.not(),
			)
			.to_slice(),
		just('\\').ignore_then(just("```")),
	))
	.repeated()
	.at_least(1)
	.collect::<String>()
	.map(FmlCodeBlockValue::Str);

	let code_block_body = choice((linebreak.to(FmlCodeBlockValue::Linebreak), body_str))
		.repeated()
		.at_least(1)
		.collect::<Vec<_>>();

	ident()
		.map(ToString::to_string)
		.or_not()
		.then_ignore(inline_whitespace().then(newline().or_not()))
		.then(code_block_body)
		.delimited_by(whitespace().then(just("```")), just("```").padded())
		.map(|(lang, body)| Token::CodeBlock { lang, body })
}

pub fn function_params<'a>()
-> impl Parser<'a, &'a str, Option<String>, Err<Simple<'a, char>>> + Clone {
	any()
		.and_is(just('>').not())
		.repeated()
		.at_least(1)
		.collect::<String>()
		.delimited_by(just('<'), just('>'))
		.or_not()
}

pub fn function_body_pat<'a>() -> impl Parser<'a, &'a str, &'a str, Err<Simple<'a, char>>> + Clone {
	recursive(|fbp| {
		choice((
			any()
				.and_is(
					choice((
						just('\\'),
						just('{').padded(),
						whitespace().ignore_then(just('}')),
					))
					.not(),
				)
				.repeated()
				.at_least(1)
				.to_slice(),
			just('\\').ignore_then(one_of("{}")).to_slice(),
			fbp,
		))
		.repeated()
		.at_least(1)
		.to_slice()
		.delimited_by(just('{').padded(), whitespace().then(just('}')))
	})
}

pub fn is_blank(input: &str) -> bool {
	if input.len() == 0 {
		true
	} else {
		for c in input.chars() {
			if !unicode::is_whitespace(c) {
				return false;
			}
		}
		true
	}
}