textlane_html/actions/hast/
math.rs1use textlane_core::hast::{Element, Node, Property, PropertyItem, PropertyValue};
2use textlane_core::{Allocator, ArenaVec};
3
4use crate::helpers::prose::{create_text, replace_prose};
5use crate::models::options::Math;
6
7pub fn mark_math<'a>(node: &mut Node<'a>, options: &Math, alloc: &'a Allocator) {
10 replace_prose(
11 node,
12 &options.ignore,
13 &options.ignore_attributes,
14 alloc,
15 |text, out| {
16 if !text.contains('$') {
17 return false;
18 }
19 let segments = split_math(text, options.single_dollar_text_math);
20 if !segments
21 .iter()
22 .any(|segment| !matches!(segment, Segment::Text(_)))
23 {
24 return false;
25 }
26 let mut plain = String::new();
27 for segment in segments {
28 match segment {
29 Segment::Text(text) => plain.push_str(&text),
30 Segment::Dollar => plain.push('$'),
31 Segment::Math(latex, display) => {
32 if !plain.is_empty() {
33 out.push(create_text(&std::mem::take(&mut plain), alloc));
34 }
35 out.push(create_math(&latex, display, alloc));
36 }
37 }
38 }
39 if !plain.is_empty() {
40 out.push(create_text(&plain, alloc));
41 }
42 true
43 },
44 );
45}
46
47enum Segment {
48 Text(String),
49 Math(String, bool),
50 Dollar,
51}
52
53fn split_math(text: &str, single: bool) -> Vec<Segment> {
54 let mut segments = vec![];
55 let mut plain = String::new();
56 let mut rest = text;
57 while let Some(at) = rest.find('$') {
58 plain.push_str(&rest[..at]);
59 let after = &rest[at..];
60 if plain.ends_with('\\') {
61 plain.pop();
62 segments.push(Segment::Text(std::mem::take(&mut plain)));
63 segments.push(Segment::Dollar);
64 rest = &after[1..];
65 continue;
66 }
67 let found = match after.strip_prefix("$$") {
68 Some(inner) => inner.find("$$").and_then(|end| {
69 let latex = inner[..end].trim();
70 (!latex.is_empty()).then(|| (latex.to_owned(), true, end + 4))
71 }),
72 None if single => find_inline(&after[1..])
73 .map(|(latex, end)| (latex.to_owned(), false, end + 1)),
74 None => None,
75 };
76 match found {
77 Some((latex, display, length)) => {
78 segments.push(Segment::Text(std::mem::take(&mut plain)));
79 segments.push(Segment::Math(latex, display));
80 rest = &after[length..];
81 }
82 None => {
83 plain.push('$');
84 rest = &after[1..];
85 }
86 }
87 }
88 plain.push_str(rest);
89 segments.push(Segment::Text(plain));
90 segments
91}
92
93fn find_inline(text: &str) -> Option<(&str, usize)> {
96 if text.is_empty() || text.starts_with(char::is_whitespace) {
97 return None;
98 }
99 let mut search = 0;
100 while let Some(offset) = text[search..].find('$') {
101 let end = search + offset;
102 let before = text[..end].chars().next_back();
103 let after = text[end + 1..].chars().next();
104 let closes = before.is_some_and(|char| !char.is_whitespace() && char != '\\')
105 && !after.is_some_and(|char| char.is_ascii_digit());
106 if closes && end > 0 {
107 return Some((&text[..end], end + 1));
108 }
109 search = end + 1;
110 }
111 None
112}
113
114fn create_math<'a>(latex: &str, display: bool, alloc: &'a Allocator) -> Node<'a> {
115 let mut classes = ArenaVec::with_capacity_in(2, alloc);
116 classes.push(PropertyItem::String("language-math"));
117 classes.push(PropertyItem::String(match display {
118 true => "math-display",
119 false => "math-inline",
120 }));
121 let mut properties = ArenaVec::with_capacity_in(1, alloc);
122 properties.push(Property {
123 name: "className",
124 value: PropertyValue::List(classes),
125 });
126 let mut children = ArenaVec::with_capacity_in(1, alloc);
127 children.push(create_text(latex, alloc));
128 Node::Element(Element {
129 tag_name: "code",
130 properties,
131 children,
132 position: None,
133 data: None,
134 content: None,
135 })
136}
137
138#[cfg(test)]
139#[path = "math_unit.rs"]
140mod unit;