organic/src/parser/text_markup.rs
Tom Alexander 720afa5d32
Update getting the previous character and previous line.
This can be done a lot more efficiently now that we are keeping track of this information in the wrapped input type instead of having to fetch to the original document out of the context tree.
2023-08-24 16:56:07 -04:00

365 lines
13 KiB
Rust

use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::multispace1;
use nom::character::complete::one_of;
use nom::character::complete::space0;
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many_till;
use nom::sequence::terminated;
#[cfg(feature = "tracing")]
use tracing::span;
use super::org_source::OrgSource;
use super::radio_link::RematchObject;
use super::Context;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::exiting::ExitClass;
use crate::parser::object_parser::standard_set_object;
use crate::parser::parser_context::ContextElement;
use crate::parser::parser_context::ExitMatcherNode;
use crate::parser::parser_with_context::parser_with_context;
use crate::parser::radio_link::rematch_target;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
use crate::parser::util::preceded_by_whitespace;
use crate::parser::Bold;
use crate::parser::Code;
use crate::parser::Italic;
use crate::parser::Object;
use crate::parser::StrikeThrough;
use crate::parser::Underline;
use crate::parser::Verbatim;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn text_markup<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
alt((
map(parser_with_context!(bold)(context), Object::Bold),
map(parser_with_context!(italic)(context), Object::Italic),
map(parser_with_context!(underline)(context), Object::Underline),
map(
parser_with_context!(strike_through)(context),
Object::StrikeThrough,
),
map(parser_with_context!(verbatim)(context), Object::Verbatim),
map(parser_with_context!(code)(context), Object::Code),
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn bold<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Bold<'s>> {
let text_markup_object_specialized = text_markup_object("*");
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Bold {
source: source.into(),
children,
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn italic<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Italic<'s>> {
let text_markup_object_specialized = text_markup_object("/");
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Italic {
source: source.into(),
children,
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn underline<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Underline<'s>> {
let text_markup_object_specialized = text_markup_object("_");
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Underline {
source: source.into(),
children,
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn strike_through<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, StrikeThrough<'s>> {
let text_markup_object_specialized = text_markup_object("+");
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
StrikeThrough {
source: source.into(),
children,
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn verbatim<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Verbatim<'s>> {
let text_markup_string_specialized = text_markup_string("=");
let (remaining, contents) = text_markup_string_specialized(context, input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Verbatim {
source: source.into(),
contents: contents.into(),
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn code<'r, 's>(
context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Code<'s>> {
let text_markup_string_specialized = text_markup_string("~");
let (remaining, contents) = text_markup_string_specialized(context, input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Code {
source: source.into(),
contents: contents.into(),
},
))
}
fn text_markup_object(
marker_symbol: &str,
) -> impl for<'r, 's> Fn(Context<'r, 's>, OrgSource<'s>) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
let marker_symbol = marker_symbol.to_owned();
move |context: Context, input: OrgSource<'_>| {
_text_markup_object(context, input, marker_symbol.as_str())
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _text_markup_object<'r, 's, 'x>(
context: Context<'r, 's>,
input: OrgSource<'s>,
marker_symbol: &'x str,
) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
let (remaining, _) = pre(context, input)?;
let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into());
let parser_context =
context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Beta,
exit_matcher: &text_markup_end_specialized,
}));
let (remaining, (children, _exit_contents)) = verify(
many_till(
parser_with_context!(standard_set_object)(&parser_context),
parser_with_context!(exit_matcher_parser)(&parser_context),
),
|(children, _exit_contents)| !children.is_empty(),
)(remaining)?;
{
#[cfg(feature = "tracing")]
let span = span!(tracing::Level::DEBUG, "Checking parent exit.");
#[cfg(feature = "tracing")]
let _enter = span.enter();
if exit_matcher_parser(context, remaining).is_ok() {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Parent exit matcher is triggering.".into(),
))));
}
}
let (remaining, _close) = text_markup_end_specialized(context, remaining)?;
let (remaining, _trailing_whitespace) = space0(remaining)?;
Ok((remaining, children))
}
fn text_markup_string(
marker_symbol: &str,
) -> impl for<'r, 's> Fn(Context<'r, 's>, OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
let marker_symbol = marker_symbol.to_owned();
move |context: Context, input: OrgSource<'_>| {
_text_markup_string(context, input, marker_symbol.as_str())
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _text_markup_string<'r, 's, 'x>(
context: Context<'r, 's>,
input: OrgSource<'s>,
marker_symbol: &'x str,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _) = pre(context, input)?;
let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into());
let parser_context =
context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Beta,
exit_matcher: &text_markup_end_specialized,
}));
let (remaining, contents) = recognize(verify(
many_till(
anychar,
parser_with_context!(exit_matcher_parser)(&parser_context),
),
|(children, _exit_contents)| !children.is_empty(),
))(remaining)?;
{
#[cfg(feature = "tracing")]
let span = span!(tracing::Level::DEBUG, "Checking parent exit.");
#[cfg(feature = "tracing")]
let _enter = span.enter();
if exit_matcher_parser(context, remaining).is_ok() {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Parent exit matcher is triggering.".into(),
))));
}
}
let (remaining, _close) = text_markup_end_specialized(context, remaining)?;
let (remaining, _trailing_whitespace) = space0(remaining)?;
Ok((remaining, contents))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn pre<'r, 's>(context: Context<'r, 's>, input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
let preceding_character = input.get_preceding_character();
match preceding_character {
// If None, we are at the start of the file which is technically the beginning of a line.
None | Some('\r') | Some('\n') | Some(' ') | Some('\t') | Some('-') | Some('(')
| Some('{') | Some('\'') | Some('"') | Some('<') => {}
Some(_) => {
// Not at start of line, cannot be a heading
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre character for text markup.".into(),
))));
}
};
Ok((input, ()))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn post<'r, 's>(context: Context<'r, 's>, input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
let (remaining, _) = alt((recognize(one_of(" \r\n\t-.,;:!?')}[\">")), line_ending))(input)?;
Ok((remaining, ()))
}
fn text_markup_end(
marker_symbol: &str,
) -> impl for<'r, 's> Fn(Context<'r, 's>, OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
let marker_symbol = marker_symbol.to_owned();
move |context: Context, input: OrgSource<'_>| {
_text_markup_end(context, input, marker_symbol.as_str())
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _text_markup_end<'r, 's, 'x>(
context: Context<'r, 's>,
input: OrgSource<'s>,
marker_symbol: &'x str,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
not(parser_with_context!(preceded_by_whitespace)(context))(input)?;
let (remaining, _marker) = terminated(
tag(marker_symbol),
peek(parser_with_context!(post)(context)),
)(input)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
impl<'x> RematchObject<'x> for Bold<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'r, 's>(
&'x self,
_context: Context<'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "*", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::Bold(Bold {
source: source.into(),
children,
}),
))
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _rematch_text_markup_object<'r, 's, 'x>(
context: Context<'r, 's>,
input: OrgSource<'s>,
marker_symbol: &'static str,
original_match_children: &'x Vec<Object<'x>>,
) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
let (remaining, _) = pre(context, input)?;
let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into());
let parser_context =
context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Beta,
exit_matcher: &text_markup_end_specialized,
}));
let (remaining, children) =
// TODO: This doesn't really check the exit matcher between each object. I think it may be possible to construct an org document that parses incorrectly with the current code.
rematch_target(&parser_context, original_match_children, remaining)?;
{
#[cfg(feature = "tracing")]
let span = span!(tracing::Level::DEBUG, "Checking parent exit.");
#[cfg(feature = "tracing")]
let _enter = span.enter();
if exit_matcher_parser(context, remaining).is_ok() {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Parent exit matcher is triggering.".into(),
))));
}
}
let (remaining, _close) = text_markup_end_specialized(context, remaining)?;
let (remaining, _trailing_whitespace) = space0(remaining)?;
Ok((remaining, children))
}