diff --git a/src/elements/latex.rs b/src/elements/latex.rs new file mode 100644 index 0000000..9832a99 --- /dev/null +++ b/src/elements/latex.rs @@ -0,0 +1,195 @@ +use nom::{ + bytes::complete::{tag, take_until}, + sequence::delimited, + IResult, +}; +use std::borrow::Cow; + +#[cfg_attr(feature = "ser", derive(serde::Serialize))] +#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Clone)] +pub struct LatexEnvironment<'a> { + pub contents: Cow<'a, str>, + pub argument: Cow<'a, str>, + pub inline: bool, +} + +impl<'a> LatexEnvironment<'a> { + #[inline] + pub fn parse(input: &str) -> Option<(&str, LatexEnvironment)> { + let bytes = input.as_bytes(); + match (bytes[0], bytes[1]) { + (b'\\', b'[') => parse_internal_delimited(input, "\\[", "\\]", false).ok(), + (b'\\', b'(') => parse_internal_delimited(input, "\\(", "\\)", true).ok(), + (b'\\', _) => parse_internal_environment(input).ok(), + (b'$', b'$') => parse_internal_delimited(input, "$$", "$$", false).ok(), + (b'$', _) => parse_internal_delimited(input, "$", "$", true).ok(), + _ => None, + } + } + + pub fn into_owned(self) -> LatexEnvironment<'static> { + LatexEnvironment { + contents: self.contents.into_owned().into(), + argument: self.argument.into_owned().into(), + inline: self.inline, + } + } +} + +fn parse_internal_delimited<'a>( + input: &'a str, + starts: &str, + ends: &str, + inline: bool, +) -> IResult<&'a str, LatexEnvironment<'a>, ()> { + let (input, contents) = delimited(tag(starts), take_until(ends), tag(ends))(input)?; + Ok(( + input, + LatexEnvironment { + contents: contents.trim().into(), + argument: "".into(), + inline: inline, + }, + )) +} + +fn parse_internal_environment(input: &str) -> IResult<&str, LatexEnvironment, ()> { + let (input, argument) = delimited(tag("\\begin{"), take_until("}"), tag("}"))(input)?; + let end = &format!("\\end{{{}}}", argument)[..]; + let (input, contents) = take_until(end)(input)?; + let (input, _) = tag(end)(input)?; + Ok(( + input, + LatexEnvironment { + contents: contents.trim().into(), + argument: argument.into(), + inline: false, + }, + )) +} + +#[test] +fn parse_environment() { + assert_eq!( + LatexEnvironment::parse("\\[\n\\frac{1}{3}\n\n\\]"), + Some(( + "", + LatexEnvironment { + contents: "\\frac{1}{3}".into(), + argument: "".into(), + inline: false, + } + )) + ); + + assert_eq!( + LatexEnvironment::parse("$$\n42!\n\n$$"), + Some(( + "", + LatexEnvironment { + contents: "42!".into(), + argument: "".into(), + inline: false, + } + )) + ); + + assert_eq!( + LatexEnvironment::parse( + r#"\begin{equation} +\int^{a}_{b}\,f(x)\,dx +\end{equation}"# + ), + Some(( + "", + LatexEnvironment { + contents: "\\int^{a}_{b}\\,f(x)\\,dx".into(), + argument: "equation".into(), + inline: false, + } + )) + ); + + assert_eq!( + LatexEnvironment::parse( + r#"\begin{equation} +\int^{a}_{b}\,f(x)\,dx +\end{align}"# + ), + None + ); +} + +#[test] +fn parse_inline() { + assert_eq!( + LatexEnvironment::parse("$\\frac{1}{3}$"), + Some(( + "", + LatexEnvironment { + contents: "\\frac{1}{3}".into(), + argument: "".into(), + inline: true, + } + )) + ); + + assert_eq!( + LatexEnvironment::parse("\\(\\frac{1}{3}\\)"), + Some(( + "", + LatexEnvironment { + contents: "\\frac{1}{3}".into(), + argument: "".into(), + inline: true, + } + )) + ); + assert_eq!( + LatexEnvironment::parse("$ text with spaces $"), + Some(( + "", + LatexEnvironment { + contents: "text with spaces".into(), + argument: "".into(), + inline: true, + } + )) + ); + assert_eq!( + LatexEnvironment::parse("\\( text with spaces \\)"), + Some(( + "", + LatexEnvironment { + contents: "text with spaces".into(), + argument: "".into(), + inline: true, + } + )) + ); + assert_eq!( + LatexEnvironment::parse("$ LaTeXxxx$"), + Some(( + "", + LatexEnvironment { + contents: "LaTeXxxx".into(), + argument: "".into(), + inline: true, + } + )) + ); + assert_eq!( + LatexEnvironment::parse("\\(b\nol\nd\\)"), + Some(( + "", + LatexEnvironment { + contents: "b\nol\nd".into(), + argument: "".into(), + inline: true, + } + )) + ); + assert_eq!(LatexEnvironment::parse("$$b\nol\nd*"), None); + assert_eq!(LatexEnvironment::parse("$b\nol\nd*"), None); +} diff --git a/src/elements/mod.rs b/src/elements/mod.rs index ff99f76..8650895 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod fn_ref; pub(crate) mod inline_call; pub(crate) mod inline_src; pub(crate) mod keyword; +pub(crate) mod latex; pub(crate) mod link; pub(crate) mod list; pub(crate) mod macros; @@ -41,6 +42,7 @@ pub use self::{ inline_call::InlineCall, inline_src::InlineSrc, keyword::{BabelCall, Keyword}, + latex::LatexEnvironment, link::Link, list::{List, ListItem}, macros::Macros, @@ -85,6 +87,7 @@ pub enum Element<'a> { Link(Link<'a>), List(List), ListItem(ListItem<'a>), + LatexEnvironment(LatexEnvironment<'a>), Macros(Macros<'a>), Snippet(Snippet<'a>), Text { value: Cow<'a, str> }, @@ -137,6 +140,7 @@ impl Element<'_> { use Element::*; match self { + LatexEnvironment(e) => LatexEnvironment(e.into_owned()), SpecialBlock(e) => SpecialBlock(e.into_owned()), QuoteBlock(e) => QuoteBlock(e.into_owned()), CenterBlock(e) => CenterBlock(e.into_owned()), diff --git a/src/export/html.rs b/src/export/html.rs index e0b0bd1..ccf1ec0 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -77,6 +77,19 @@ impl HtmlHandler for DefaultHtmlHandler { write!(w, "