1use crate::node::{Node, NodeKind, StackKind};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub enum Style {
18 Display,
20 Text,
22 Script,
24 ScriptScript,
26}
27
28impl Style {
29 #[must_use]
32 pub const fn is_script(self) -> bool {
33 matches!(self, Self::Script | Self::ScriptScript)
34 }
35
36 #[must_use]
39 pub const fn size_factor(self) -> f64 {
40 match self {
41 Self::Display | Self::Text => 1.0,
42 Self::Script => 0.7,
43 Self::ScriptScript => 0.5,
44 }
45 }
46
47 #[must_use]
49 pub const fn sup(self) -> Self {
50 match self {
51 Self::Display | Self::Text => Self::Script,
52 Self::Script | Self::ScriptScript => Self::ScriptScript,
53 }
54 }
55
56 #[must_use]
58 pub const fn num(self) -> Self {
59 match self {
60 Self::Display => Self::Text,
61 Self::Text => Self::Script,
62 Self::Script | Self::ScriptScript => Self::ScriptScript,
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70pub struct StyleCtx {
71 pub style: Style,
73 pub cramped: bool,
76}
77
78impl StyleCtx {
79 #[must_use]
81 pub const fn new(style: Style) -> Self {
82 Self {
83 style,
84 cramped: false,
85 }
86 }
87
88 #[must_use]
90 pub const fn display() -> Self {
91 Self::new(Style::Display)
92 }
93
94 #[must_use]
97 pub const fn sup(self) -> Self {
98 Self {
99 style: self.style.sup(),
100 cramped: self.cramped,
101 }
102 }
103
104 #[must_use]
106 pub const fn sub(self) -> Self {
107 Self {
108 style: self.style.sup(),
109 cramped: true,
110 }
111 }
112
113 #[must_use]
116 pub const fn num(self) -> Self {
117 Self {
118 style: self.style.num(),
119 cramped: self.cramped,
120 }
121 }
122
123 #[must_use]
126 pub const fn den(self) -> Self {
127 Self {
128 style: self.style.num(),
129 cramped: true,
130 }
131 }
132
133 #[must_use]
135 pub const fn cramp(self) -> Self {
136 Self {
137 style: self.style,
138 cramped: true,
139 }
140 }
141
142 #[must_use]
144 pub const fn size_factor(self) -> f64 {
145 self.style.size_factor()
146 }
147}
148
149pub fn style_walk<'a, F>(node: &'a Node, ctx: StyleCtx, visit: &mut F)
172where
173 F: FnMut(&'a Node, StyleCtx),
174{
175 visit(node, ctx);
176 match &node.kind {
177 NodeKind::List(items) => walk_list(items, ctx, visit),
178 NodeKind::Scripts { base, sub, sup, .. } => {
179 if let Some(b) = base {
180 style_walk(b, ctx, visit);
181 }
182 if let Some(s) = sup {
183 style_walk(s, ctx.sup(), visit);
184 }
185 if let Some(s) = sub {
186 style_walk(s, ctx.sub(), visit);
187 }
188 }
189 NodeKind::Frac { num, den, spec } => {
190 let eff = match spec.forced_style {
191 Some(forced) => StyleCtx {
192 style: forced,
193 cramped: ctx.cramped,
194 },
195 None => ctx,
196 };
197 style_walk(num, eff.num(), visit);
198 style_walk(den, eff.den(), visit);
199 }
200 NodeKind::Radical { index, radicand } => {
201 if let Some(ix) = index {
202 style_walk(
203 ix,
204 StyleCtx {
205 style: Style::ScriptScript,
206 cramped: ctx.cramped,
207 },
208 visit,
209 );
210 }
211 style_walk(radicand, ctx.cramp(), visit);
212 }
213 NodeKind::Accent { base, .. } => style_walk(base, ctx.cramp(), visit),
214 NodeKind::LeftRight { body, .. } => walk_list(body, ctx, visit),
215 NodeKind::Text { body } | NodeKind::TextStyled { body, .. } => {
216 walk_list(body, ctx, visit);
217 }
218 NodeKind::MathIsland { body, display } => {
219 let style = if *display {
220 Style::Display
221 } else {
222 Style::Text
223 };
224 walk_list(body, StyleCtx::new(style), visit);
225 }
226 NodeKind::MathFont { body, .. } | NodeKind::Phantom { body, .. } => {
227 style_walk(body, ctx, visit);
228 }
229 NodeKind::Stack {
230 kind,
231 annotation,
232 base,
233 } => {
234 let ann_ctx = match kind {
235 StackKind::Underset => ctx.sub(),
236 StackKind::Stackrel | StackKind::Overset => ctx.sup(),
237 };
238 style_walk(annotation, ann_ctx, visit);
239 style_walk(base, ctx, visit);
240 }
241 NodeKind::XArrow { above, below, .. } => {
242 style_walk(above, ctx.sup(), visit);
245 if let Some(below) = below {
246 style_walk(below, ctx.sub(), visit);
247 }
248 }
249 NodeKind::AlignBlock { lines, .. } => {
250 for line in lines {
252 style_walk(line, ctx, visit);
253 }
254 }
255 NodeKind::Environment { name, rows, .. } => {
256 let cell_ctx = if name == "substack" {
257 StyleCtx::new(Style::Script)
260 } else if name.starts_with("align") {
261 ctx
262 } else {
263 StyleCtx::new(Style::Text)
264 };
265 for row in rows {
266 for cell in row {
267 style_walk(cell, cell_ctx, visit);
268 }
269 }
270 }
271 NodeKind::Symbol { .. }
272 | NodeKind::BigOp { .. }
273 | NodeKind::OpName { .. }
274 | NodeKind::SizedDelim { .. }
275 | NodeKind::TextRun { .. }
276 | NodeKind::StyleChange(_)
277 | NodeKind::SizeChange(_)
278 | NodeKind::AlignChange(_)
279 | NodeKind::LineSpacing(_)
280 | NodeKind::ColorChange(_)
281 | NodeKind::Space(_)
282 | NodeKind::Tie
283 | NodeKind::Linebreak
284 | NodeKind::AlignTab
285 | NodeKind::Fragment(_) => {}
286 }
287}
288
289fn walk_list<'a, F>(items: &'a [Node], mut ctx: StyleCtx, visit: &mut F)
293where
294 F: FnMut(&'a Node, StyleCtx),
295{
296 for item in items {
297 if let NodeKind::StyleChange(style) = &item.kind {
298 visit(item, ctx);
299 ctx = StyleCtx {
300 style: *style,
301 cramped: ctx.cramped,
302 };
303 continue;
304 }
305 style_walk(item, ctx, visit);
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn sup_chain() {
315 assert_eq!(Style::Display.sup(), Style::Script);
316 assert_eq!(Style::Text.sup(), Style::Script);
317 assert_eq!(Style::Script.sup(), Style::ScriptScript);
318 assert_eq!(Style::ScriptScript.sup(), Style::ScriptScript);
319 }
320
321 #[test]
322 fn num_chain() {
323 assert_eq!(Style::Display.num(), Style::Text);
324 assert_eq!(Style::Text.num(), Style::Script);
325 assert_eq!(Style::Script.num(), Style::ScriptScript);
326 assert_eq!(Style::ScriptScript.num(), Style::ScriptScript);
327 }
328
329 #[test]
330 fn sub_is_cramped_sup() {
331 let ctx = StyleCtx::display();
332 assert_eq!(ctx.sub().style, Style::Script);
333 assert!(ctx.sub().cramped);
334 assert!(!ctx.sup().cramped);
335 }
336
337 #[test]
338 fn den_is_cramped_num() {
339 let ctx = StyleCtx::new(Style::Text);
340 assert_eq!(ctx.den().style, Style::Script);
341 assert!(ctx.den().cramped);
342 }
343
344 #[test]
345 fn size_factors_are_cm_10_7_5() {
346 assert_eq!(Style::Display.size_factor(), 1.0);
347 assert_eq!(Style::Text.size_factor(), 1.0);
348 assert_eq!(Style::Script.size_factor(), 0.7);
349 assert_eq!(Style::ScriptScript.size_factor(), 0.5);
350 }
351}