Documents: inline formulas typeset in text; \( and \[ read as math
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / site (push) Successful in 35s
CI / crate (push) Failing after 35s
CI / platforms (windows-latest) (push) Successful in 5m41s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-18 22:42:07 -04:00
1 parent 7d704d97f4
commit dceca9f3c6
12 files changed
+569 -61

No files matched your search

+78 -5
View File
@@ -1,4 +1,4 @@
use std::ops::Range;
use std::{borrow::Cow, ops::Range};
use gpui::App;
use markdown::{Constructs, ParseOptions, mdast};
@@ -20,19 +20,92 @@ pub(crate) fn options() -> ParseOptions {
/// `text` as a syntax tree.
pub(crate) fn tree(text: &str) -> mdast::Node {
markdown::to_mdast(text, &options()).expect("markdown without MDX always parses")
parsed(text, &options())
}
fn parsed(text: &str, options: &ParseOptions) -> mdast::Node {
let text = tex_math(text, options);
markdown::to_mdast(&text, options).expect("markdown without MDX always parses")
}
/// `\(` and `\[` delimiters as `$`, byte for byte.
fn tex_math<'a>(text: &'a str, options: &ParseOptions) -> Cow<'a, str> {
if !text.contains("\\(") && !text.contains("\\[") {
return Cow::Borrowed(text);
}
let root = markdown::to_mdast(text, options).expect("markdown without MDX always parses");
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let mut kept = vec![false; text.len()];
for node in nodes {
use mdast::Node::{Code, Html, InlineCode, InlineMath, Math, Toml, Yaml};
if matches!(
node,
Code(_) | InlineCode(_) | Math(_) | InlineMath(_) | Html(_) | Yaml(_) | Toml(_)
) {
kept[span(node)].fill(true);
}
}
let bytes = text.as_bytes();
let mut out = bytes.to_vec();
let mut open: Option<(usize, u8)> = None;
let mut at = 0;
while at + 1 < bytes.len() {
match bytes[at] {
// A blank line ends a paragraph and its formula.
b'\n' if blank_after(&bytes[at + 1..]) => open = None,
b'\\' if !kept[at] => {
match (bytes[at + 1], open) {
(b'(', None) => open = Some((at, b')')),
(b'[', None) => open = Some((at, b']')),
(close, Some((start, wanted)))
if close == wanted && at > start + 2 && !text[start..at].contains('$') =>
{
out[start..start + 2].copy_from_slice(b"$ ");
out[at..at + 2].copy_from_slice(b" $");
open = None;
}
_ => {}
}
// An escaped backslash escapes nothing after it.
at += 2;
continue;
}
_ => {}
}
at += 1;
}
match out == bytes {
true => Cow::Borrowed(text),
false => Cow::Owned(String::from_utf8(out).expect("ASCII swaps keep UTF-8")),
}
}
/// Whether the line `rest` starts holds only white space.
fn blank_after(rest: &[u8]) -> bool {
rest.iter()
.take_while(|byte| **byte != b'\n')
.all(u8::is_ascii_whitespace)
}
/// An inline formula's TeX: inside `$` runs or `\(` pairs.
pub(crate) fn math_inside(source: &str, whole: Range<usize>) -> Range<usize> {
match source[whole.clone()].starts_with('$') {
true => crate::documents::tree::inside(source, whole, '$'),
false => whole.start + 2..whole.end - 2,
}
}
/// How a whole file parses: front matter too.
pub fn file_options() -> ParseOptions {
pub(crate) fn file_options() -> ParseOptions {
let mut options = options();
options.constructs.frontmatter = true;
options
}
/// A whole file as a syntax tree.
pub(crate) fn file_tree(text: &str) -> mdast::Node {
markdown::to_mdast(text, &file_options()).expect("markdown without MDX always parses")
pub fn file_tree(text: &str) -> mdast::Node {
parsed(text, &file_options())
}
/// The bytes of the source a parsed node spans.