Skip to content

Commit 9ed45b4

Browse files
authored
Add support for serializing math nodes as markdown
Closes GH-148. Reviewed-by: Titus Wormer <[email protected]>
1 parent e7ed526 commit 9ed45b4

File tree

9 files changed

+505
-14
lines changed

9 files changed

+505
-14
lines changed

mdast_util_to_markdown/src/configure.rs

+6
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ pub struct Options {
6565
/// Setext headings cannot be used for empty headings or headings with a
6666
/// rank of three or more.
6767
pub setext: bool,
68+
/// Whether to support math (text) with a single dollar (`bool`, default: `true`).
69+
/// Single dollars work in Pandoc and many other places, but often interfere with “normal”
70+
/// dollars in text.
71+
/// If you turn this off, you can still use two or more dollars for text math.
72+
pub single_dollar_text_math: bool,
6873
/// Marker to use for strong (`'*'` or `'_'`, default: `'*'`).
6974
pub strong: char,
7075
/// Whether to join definitions without a blank line (`bool`, default:
@@ -90,6 +95,7 @@ impl Default for Options {
9095
rule_repetition: 3,
9196
rule_spaces: false,
9297
setext: false,
98+
single_dollar_text_math: true,
9399
strong: '*',
94100
tight_definitions: false,
95101
}

mdast_util_to_markdown/src/construct_name.rs

+27-7
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ pub enum ConstructName {
2020
/// ^
2121
/// ```
2222
Blockquote,
23-
/// Whole code (indented).
24-
///
25-
/// ```markdown
26-
/// ␠␠␠␠console.log(1)
27-
/// ^^^^^^^^^^^^^^^^^^
28-
/// ```
29-
CodeIndented,
3023
/// Whole code (fenced).
3124
///
3225
/// ````markdown
@@ -74,6 +67,13 @@ pub enum ConstructName {
7467
/// | ~~~
7568
/// ````
7669
CodeFencedMetaTilde,
70+
/// Whole code (indented).
71+
///
72+
/// ```markdown
73+
/// ␠␠␠␠console.log(1)
74+
/// ^^^^^^^^^^^^^^^^^^
75+
/// ```
76+
CodeIndented,
7777
/// Whole definition.
7878
///
7979
/// ```markdown
@@ -186,6 +186,26 @@ pub enum ConstructName {
186186
/// ^^^^
187187
/// ```
188188
ListItem,
189+
/// Math (flow).
190+
///
191+
/// ```markdown
192+
/// > | $$
193+
/// ^^
194+
/// > | a
195+
/// ^
196+
/// > | $$
197+
/// ^^
198+
/// ```
199+
MathFlow,
200+
/// Math (flow) meta flag.
201+
///
202+
/// ```markdown
203+
/// > | $$a
204+
/// ^
205+
/// | b
206+
/// | $$
207+
/// ```
208+
MathFlowMeta,
189209
/// Paragraph.
190210
///
191211
/// ```markdown
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! JS equivalent: https://github.com/syntax-tree/mdast-util-math/blob/main/lib/index.js#L241
2+
3+
use super::Handle;
4+
use crate::state::{Info, State};
5+
use alloc::format;
6+
use markdown::{
7+
mdast::{InlineMath, Node},
8+
message::Message,
9+
};
10+
use regex::Regex;
11+
12+
impl Handle for InlineMath {
13+
fn handle(
14+
&self,
15+
state: &mut State,
16+
_info: &Info,
17+
_parent: Option<&Node>,
18+
_node: &Node,
19+
) -> Result<alloc::string::String, Message> {
20+
let mut size: usize = if !state.options.single_dollar_text_math {
21+
2
22+
} else {
23+
1
24+
};
25+
26+
let pattern = format!("(^|[^$]){}([^$]|$)", "\\$".repeat(size));
27+
let mut dollar_sign_match = Regex::new(&pattern).unwrap();
28+
while dollar_sign_match.is_match(&self.value) {
29+
size += 1;
30+
let pattern = format!("(^|[^$]){}([^$]|$)", "\\$".repeat(size));
31+
dollar_sign_match = Regex::new(&pattern).unwrap();
32+
}
33+
34+
let sequence = "$".repeat(size);
35+
36+
let no_whitespaces = !self.value.chars().all(char::is_whitespace);
37+
let starts_with_whitespace = self.value.starts_with(char::is_whitespace);
38+
let ends_with_whitespace = self.value.ends_with(char::is_whitespace);
39+
let starts_with_dollar = self.value.starts_with('$');
40+
let ends_with_dollar = self.value.ends_with('$');
41+
42+
let mut value = self.value.clone();
43+
if no_whitespaces
44+
&& ((starts_with_whitespace && ends_with_whitespace)
45+
|| starts_with_dollar
46+
|| ends_with_dollar)
47+
{
48+
value = format!(" {} ", value);
49+
}
50+
51+
for pattern in &mut state.r#unsafe {
52+
if !pattern.at_break {
53+
continue;
54+
}
55+
56+
State::compile_pattern(pattern);
57+
58+
if let Some(regex) = &pattern.compiled {
59+
while let Some(m) = regex.find(&value) {
60+
let position = m.start();
61+
62+
let position = if position > 0
63+
&& &value[position..m.len()] == "\n"
64+
&& &value[position - 1..position] == "\r"
65+
{
66+
position - 1
67+
} else {
68+
position
69+
};
70+
71+
value.replace_range(position..m.start() + 1, " ");
72+
}
73+
}
74+
}
75+
76+
Ok(format!("{}{}{}", sequence, value, sequence))
77+
}
78+
}
79+
80+
pub fn peek_inline_math() -> char {
81+
'$'
82+
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! JS equivalent: https://github.com/syntax-tree/mdast-util-math/blob/main/lib/index.js#L204
2+
3+
use super::Handle;
4+
use crate::{
5+
construct_name::ConstructName,
6+
state::{Info, State},
7+
util::{longest_char_streak::longest_char_streak, safe::SafeConfig},
8+
};
9+
use alloc::string::String;
10+
use markdown::{
11+
mdast::{Math, Node},
12+
message::Message,
13+
};
14+
15+
impl Handle for Math {
16+
fn handle(
17+
&self,
18+
state: &mut State,
19+
_info: &Info,
20+
_parent: Option<&Node>,
21+
_node: &Node,
22+
) -> Result<alloc::string::String, Message> {
23+
let sequence = "$".repeat((longest_char_streak(&self.value, '$') + 1).max(2));
24+
state.enter(ConstructName::MathFlow);
25+
26+
let mut value = String::new();
27+
value.push_str(&sequence);
28+
29+
if let Some(meta) = &self.meta {
30+
state.enter(ConstructName::MathFlowMeta);
31+
value.push_str(&state.safe(meta, &SafeConfig::new(&value, "\n", Some('$'))));
32+
state.exit();
33+
}
34+
35+
value.push('\n');
36+
37+
if !self.value.is_empty() {
38+
value.push_str(&self.value);
39+
value.push('\n');
40+
}
41+
42+
value.push_str(&sequence);
43+
state.exit();
44+
Ok(value)
45+
}
46+
}

mdast_util_to_markdown/src/handle/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ pub mod html;
1212
pub mod image;
1313
pub mod image_reference;
1414
pub mod inline_code;
15+
pub mod inline_math;
1516
pub mod link;
1617
pub mod link_reference;
1718
mod list;
1819
mod list_item;
20+
mod math;
1921
mod paragraph;
2022
mod root;
2123
pub mod strong;

mdast_util_to_markdown/src/handle/root.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ fn phrasing(child: &Node) -> bool {
3333
*child,
3434
Node::Break(_)
3535
| Node::Emphasis(_)
36-
| Node::ImageReference(_)
3736
| Node::Image(_)
37+
| Node::ImageReference(_)
3838
| Node::InlineCode(_)
39-
| Node::LinkReference(_)
39+
| Node::InlineMath(_)
4040
| Node::Link(_)
41+
| Node::LinkReference(_)
4142
| Node::Strong(_)
4243
| Node::Text(_)
4344
)

mdast_util_to_markdown/src/state.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use crate::{
77
construct_name::ConstructName,
88
handle::{
99
emphasis::peek_emphasis, html::peek_html, image::peek_image,
10-
image_reference::peek_image_reference, inline_code::peek_inline_code, link::peek_link,
11-
link_reference::peek_link_reference, strong::peek_strong, Handle,
10+
image_reference::peek_image_reference, inline_code::peek_inline_code,
11+
inline_math::peek_inline_math, link::peek_link, link_reference::peek_link_reference,
12+
strong::peek_strong, Handle,
1213
},
1314
r#unsafe::Unsafe,
1415
util::{
@@ -322,6 +323,8 @@ impl<'a> State<'a> {
322323
Node::Strong(strong) => strong.handle(self, info, parent, node),
323324
Node::Text(text) => text.handle(self, info, parent, node),
324325
Node::ThematicBreak(thematic_break) => thematic_break.handle(self, info, parent, node),
326+
Node::Math(math) => math.handle(self, info, parent, node),
327+
Node::InlineMath(inline_math) => inline_math.handle(self, info, parent, node),
325328
_ => Err(Message {
326329
place: None,
327330
reason: format!("Unexpected node type `{:?}`", node),
@@ -409,7 +412,7 @@ impl<'a> State<'a> {
409412
index_stack: Vec::new(),
410413
options,
411414
stack: Vec::new(),
412-
r#unsafe: Unsafe::get_default_unsafe(),
415+
r#unsafe: Unsafe::get_default_unsafe(options),
413416
}
414417
}
415418

@@ -424,6 +427,7 @@ impl<'a> State<'a> {
424427
Node::LinkReference(_) => Some(peek_link_reference()),
425428
Node::Link(link) => Some(peek_link(link, node, self)),
426429
Node::Strong(_) => Some(peek_strong(self)),
430+
Node::InlineMath(_) => Some(peek_inline_math()),
427431
_ => None,
428432
}
429433
}

mdast_util_to_markdown/src/unsafe.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! JS equivalent: <https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/unsafe.js>.
44
//! Also: <https://github.com/syntax-tree/mdast-util-to-markdown/blob/fd6a508/lib/types.js#L287-L305>.
55
6-
use crate::construct_name::ConstructName;
6+
use crate::{construct_name::ConstructName, Options};
77
use alloc::{vec, vec::Vec};
88
use regex::Regex;
99

@@ -38,7 +38,7 @@ impl<'a> Unsafe<'a> {
3838
}
3939
}
4040

41-
pub fn get_default_unsafe() -> Vec<Self> {
41+
pub fn get_default_unsafe(options: &Options) -> Vec<Self> {
4242
let full_phrasing_spans = vec![
4343
ConstructName::Autolink,
4444
ConstructName::DestinationLiteral,
@@ -87,6 +87,7 @@ impl<'a> Unsafe<'a> {
8787
ConstructName::CodeFencedMetaTilde,
8888
ConstructName::DestinationLiteral,
8989
ConstructName::HeadingAtx,
90+
ConstructName::MathFlowMeta,
9091
],
9192
vec![],
9293
false,
@@ -102,6 +103,7 @@ impl<'a> Unsafe<'a> {
102103
ConstructName::CodeFencedMetaTilde,
103104
ConstructName::DestinationLiteral,
104105
ConstructName::HeadingAtx,
106+
ConstructName::MathFlowMeta,
105107
],
106108
vec![],
107109
false,
@@ -308,6 +310,27 @@ impl<'a> Unsafe<'a> {
308310
false,
309311
),
310312
Self::new('~', None, None, vec![], vec![], true),
313+
Self::new(
314+
'$',
315+
None,
316+
if options.single_dollar_text_math {
317+
None
318+
} else {
319+
"\\$".into()
320+
},
321+
vec![ConstructName::Phrasing],
322+
vec![],
323+
false,
324+
),
325+
Self::new(
326+
'$',
327+
None,
328+
None,
329+
vec![ConstructName::MathFlowMeta],
330+
vec![],
331+
false,
332+
),
333+
Self::new('$', None, "\\$".into(), vec![], vec![], true),
311334
]
312335
}
313336

0 commit comments

Comments
 (0)