From d292dd6f32232a70bee8a248f2d8dac288f8c7e5 Mon Sep 17 00:00:00 2001 From: trivernis Date: Wed, 5 Aug 2020 22:45:38 +0200 Subject: [PATCH] Add AsciiMath inline and block parsing --- Cargo.lock | 25 +++++++++++++++++++++++++ Cargo.toml | 3 ++- README.md | 18 +++++++++++++++++- src/elements/mod.rs | 13 +++++++++++++ src/elements/tokens.rs | 9 +++++++-- src/format/html.rs | 21 +++++++++++++++++++++ src/parser/block.rs | 22 +++++++++++++++++++++- src/parser/inline.rs | 17 +++++++++++++++++ 8 files changed, 123 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd0aae4..fc55745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,17 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "asciimath-rs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "charred 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "atty" version = "0.2.14" @@ -75,6 +86,11 @@ name = "charred" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "charred" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "chrono" version = "0.4.11" @@ -299,6 +315,11 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "memchr" version = "2.3.3" @@ -573,6 +594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "snekdown" version = "0.18.3" dependencies = [ + "asciimath-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "charred 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -789,6 +811,7 @@ dependencies = [ "checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum asciimath-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60e268057257d5bb6a296d0412445b54f7e36bb90d7bc0071f31310a4231c8ec" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" "checksum base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" @@ -798,6 +821,7 @@ dependencies = [ "checksum cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)" = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum charred 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cf73c7fbbaf59d5643f99c6a4413eba1b914a7489c39b730ec7d8d72e7bb061" +"checksum charred 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "81443b6f18b18560f94a0e14d529a1db379581473217b1b2a40ecae0c96a5d0e" "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" "checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" "checksum colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" @@ -826,6 +850,7 @@ dependencies = [ "checksum line-wrap 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" "checksum linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum minify 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2ef7c582bd7587da887914eaf294897e82f2f5c98b741f137f2a918cd26a885b" "checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" diff --git a/Cargo.toml b/Cargo.toml index 0ae2052..66d2213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,5 @@ gh-emoji = "1.0.3" notify = "4.0.12" toml = "0.5.6" serde ="1.0.111" -serde_derive = "1.0.111" \ No newline at end of file +serde_derive = "1.0.111" +asciimath-rs = "0.4.0" \ No newline at end of file diff --git a/README.md b/README.md index 6fc18a9..281fa76 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,22 @@ There is a book about snekdown[^book] and a github repo[^github]. Bibliography entries are only shown when used in the document. + +## Math + +Snekdown allows the embedding of [AsciiMath](http://asciimath.org/): + +``` +inline math $$ a^2 + b^2 = c^2 $$ + +Block Math +$$$ +A = [[1, 2],[3,4]] +$$$ +``` + +Currently math only get's rendered into MathML which is only supported by Safari and Firefox. + ## Roadmap The end goal is to have a markup language with features similar to LaTeX. @@ -194,7 +210,7 @@ The end goal is to have a markup language with features similar to LaTeX. - [x] Watching and rendering on change - [ ] Metadata files - [x] Bibliography -- [ ] Math +- [x] Math - [ ] Text sizes - [ ] Title pages - [ ] Glossary diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 4487948..6e63ce0 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -4,6 +4,7 @@ use crate::references::bibliography::{BibEntry, BibReference, Bibliography}; use crate::references::configuration::Configuration; use crate::references::placeholders::ProcessPlaceholders; use crate::references::templates::{Template, TemplateVariable}; +use asciimath_rs::elements::special::Expression; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; @@ -40,6 +41,7 @@ pub enum Block { List(List), Table(Table), CodeBlock(CodeBlock), + MathBlock(MathBlock), Quote(Quote), Import(Import), Placeholder(Arc>), @@ -164,6 +166,7 @@ pub enum Inline { Checkbox(Checkbox), Emoji(Emoji), Colored(Colored), + Math(Math), BibReference(Arc>), TemplateVar(Arc>), } @@ -250,6 +253,16 @@ pub struct Colored { pub(crate) color: String, } +#[derive(Clone, Debug)] +pub struct Math { + pub(crate) expression: Expression, +} + +#[derive(Clone, Debug)] +pub struct MathBlock { + pub(crate) expression: Expression, +} + // implementations impl Document { diff --git a/src/elements/tokens.rs b/src/elements/tokens.rs index 61f6c41..1b717de 100644 --- a/src/elements/tokens.rs +++ b/src/elements/tokens.rs @@ -32,6 +32,7 @@ pub(crate) const R_BRACE: char = '{'; pub(crate) const L_BRACE: char = '}'; pub(crate) const PERCENT: char = '%'; pub(crate) const COMMA: char = ','; +pub(crate) const MATH: char = '$'; // aliases @@ -71,13 +72,14 @@ pub(crate) const STRIKED: char = TILDE; pub(crate) const UNDERLINED: char = UNDERSCR; pub(crate) const SUPER: char = UP; pub(crate) const EMOJI: char = COLON; +pub(crate) const MATH_INLINE: &'static [char] = &[MATH, MATH]; pub(crate) const BOLD: [char; 2] = [ASTERISK, ASTERISK]; // groups pub(crate) const QUOTES: [char; 2] = [SINGLE_QUOTE, DOUBLE_QUOTE]; -pub(crate) const BLOCK_SPECIAL_CHARS: [&[char]; 9] = [ +pub(crate) const BLOCK_SPECIAL_CHARS: &'static [&[char]] = &[ &[HASH], &[HASH, META_OPEN], &[MINUS, SPACE], @@ -87,9 +89,10 @@ pub(crate) const BLOCK_SPECIAL_CHARS: [&[char]; 9] = [ &[META_OPEN], &[IMPORT_START, IMPORT_OPEN], &SQ_CENTERED_START, + &SQ_MATH, ]; -pub(crate) const INLINE_SPECIAL_CHARS: [char; 11] = [ +pub(crate) const INLINE_SPECIAL_CHARS: &'static [char] = &[ BACKTICK, TILDE, UNDERSCR, @@ -101,6 +104,7 @@ pub(crate) const INLINE_SPECIAL_CHARS: [char; 11] = [ SUPER, EMOJI, COLOR_START, + MATH, ]; pub(crate) const LIST_SPECIAL_CHARS: [char; 14] = [ @@ -119,3 +123,4 @@ pub(crate) const SQ_PHOLDER_STOP: [char; 2] = [PHOLDER_CLOSE, PHOLDER_CLOSE]; pub(crate) const SQ_CENTERED_START: [char; 2] = [PIPE, PIPE]; pub(crate) const SQ_COLOR_START: [char; 2] = [COLOR_START, COLOR_OPEN]; pub(crate) const SQ_BIBREF_START: [char; 2] = [BIBREF_OPEN, BIBREF_REF]; +pub(crate) const SQ_MATH: &'static [char] = &[MATH, MATH, MATH]; diff --git a/src/format/html.rs b/src/format/html.rs index cb835b6..38595e5 100644 --- a/src/format/html.rs +++ b/src/format/html.rs @@ -3,6 +3,7 @@ use crate::format::PlaceholderTemplate; use crate::references::bibliography::{BibEntry, BibReference}; use crate::references::configuration::Value; use crate::references::templates::{Template, TemplateVariable}; +use asciimath_rs::format::mathml::ToMathML; use htmlescape::{encode_attribute, encode_minimal}; use minify::html::minify; use std::cell::RefCell; @@ -64,6 +65,7 @@ impl ToHtml for Inline { Inline::Colored(colored) => colored.to_html(), Inline::BibReference(bibref) => bibref.read().unwrap().to_html(), Inline::TemplateVar(var) => var.read().unwrap().to_html(), + Inline::Math(m) => m.to_html(), } } } @@ -79,6 +81,7 @@ impl ToHtml for Block { Block::Section(section) => section.to_html(), Block::Import(import) => import.to_html(), Block::Placeholder(placeholder) => placeholder.read().unwrap().to_html(), + Block::MathBlock(m) => m.to_html(), } } } @@ -122,6 +125,24 @@ impl ToHtml for Document { } } +impl ToHtml for Math { + fn to_html(&self) -> String { + format!( + "{}", + self.expression.to_mathml() + ) + } +} + +impl ToHtml for MathBlock { + fn to_html(&self) -> String { + format!( + "{}", + self.expression.to_mathml() + ) + } +} + impl ToHtml for Import { fn to_html(&self) -> String { let anchor = self.anchor.read().unwrap(); diff --git a/src/parser/block.rs b/src/parser/block.rs index b1882a8..f31d68f 100644 --- a/src/parser/block.rs +++ b/src/parser/block.rs @@ -1,6 +1,8 @@ use super::ParseResult; use crate::elements::tokens::*; -use crate::elements::{Block, CodeBlock, Import, List, ListItem, Paragraph, Quote, Section, Table}; +use crate::elements::{ + Block, CodeBlock, Import, List, ListItem, MathBlock, Paragraph, Quote, Section, Table, +}; use crate::parser::inline::ParseInline; use crate::parser::line::ParseLine; use crate::Parser; @@ -9,6 +11,7 @@ pub(crate) trait ParseBlock { fn parse_block(&mut self) -> ParseResult; fn parse_section(&mut self) -> ParseResult
; fn parse_code_block(&mut self) -> ParseResult; + fn parse_math_block(&mut self) -> ParseResult; fn parse_quote(&mut self) -> ParseResult; fn parse_paragraph(&mut self) -> ParseResult; fn parse_list(&mut self) -> ParseResult; @@ -36,6 +39,8 @@ impl ParseBlock for Parser { Block::Table(table) } else if let Ok(code_block) = self.parse_code_block() { Block::CodeBlock(code_block) + } else if let Ok(math_block) = self.parse_math_block() { + Block::MathBlock(math_block) } else if let Ok(quote) = self.parse_quote() { Block::Quote(quote) } else if let Ok(import) = self.parse_import() { @@ -120,6 +125,21 @@ impl ParseBlock for Parser { }) } + /// parses a math block + fn parse_math_block(&mut self) -> ParseResult { + let start_index = self.ctm.get_index(); + self.ctm.seek_whitespace(); + self.ctm.assert_sequence(SQ_MATH, Some(start_index))?; + self.ctm.seek_one()?; + let text = self.ctm.get_string_until_sequence(&[SQ_MATH], &[])?; + for _ in 0..1 { + self.ctm.seek_one()?; + } + Ok(MathBlock { + expression: asciimath_rs::parse(text), + }) + } + /// parses a quote fn parse_quote(&mut self) -> ParseResult { let start_index = self.ctm.get_index(); diff --git a/src/parser/inline.rs b/src/parser/inline.rs index 25e5eaa..0db67a4 100644 --- a/src/parser/inline.rs +++ b/src/parser/inline.rs @@ -18,6 +18,7 @@ pub(crate) trait ParseInline { fn parse_bold(&mut self) -> ParseResult; fn parse_italic(&mut self) -> ParseResult; fn parse_striked(&mut self) -> ParseResult; + fn parse_math(&mut self) -> ParseResult; fn parse_monospace(&mut self) -> ParseResult; fn parse_underlined(&mut self) -> ParseResult; fn parse_superscript(&mut self) -> ParseResult; @@ -84,6 +85,8 @@ impl ParseInline for Parser { Ok(Inline::Colored(colored)) } else if let Ok(bibref) = self.parse_bibref() { Ok(Inline::BibReference(bibref)) + } else if let Ok(math) = self.parse_math() { + Ok(Inline::Math(math)) } else { Ok(Inline::Plain(self.parse_plain()?)) } @@ -195,6 +198,20 @@ impl ParseInline for Parser { }) } + fn parse_math(&mut self) -> ParseResult { + let start_index = self.ctm.get_index(); + self.ctm.assert_sequence(&MATH_INLINE, Some(start_index))?; + self.ctm.seek_one()?; + let content = self + .ctm + .get_string_until_sequence(&[MATH_INLINE, &[LB]], &[])?; + self.ctm.seek_one()?; + + Ok(Math { + expression: asciimath_rs::parse(content), + }) + } + /// parses monospace text (inline-code) that isn't allowed to contain special characters fn parse_monospace(&mut self) -> ParseResult { let start_index = self.ctm.get_index();