ftml/render/html/element/
math.rs

1/*
2 * render/html/element/math.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2025 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use super::prelude::*;
22use cfg_if::cfg_if;
23use std::num::NonZeroUsize;
24
25cfg_if! {
26    if #[cfg(feature = "mathml")] {
27        use latex2mathml::{latex_to_mathml, DisplayStyle};
28    } else {
29        /// Mocked version of the enum from `latex2mathml`.
30        #[derive(Debug, Copy, Clone, PartialEq, Eq)]
31        enum DisplayStyle {
32            Block,
33            Inline,
34        }
35    }
36}
37
38pub fn render_math_block(ctx: &mut HtmlContext, name: Option<&str>, latex_source: &str) {
39    debug!(
40        "Rendering math block (name '{}', source '{}')",
41        name.unwrap_or("<none>"),
42        latex_source,
43    );
44
45    let index = ctx.next_equation_index();
46
47    render_latex(ctx, name, Some(index), latex_source, DisplayStyle::Block);
48}
49
50pub fn render_math_inline(ctx: &mut HtmlContext, latex_source: &str) {
51    debug!("Rendering math inline (source '{latex_source}'");
52    render_latex(ctx, None, None, latex_source, DisplayStyle::Inline);
53}
54
55fn render_latex(
56    ctx: &mut HtmlContext,
57    name: Option<&str>,
58    index: Option<NonZeroUsize>,
59    latex_source: &str,
60    display: DisplayStyle,
61) {
62    // error_type is unused if MathML is disabled
63    let (html_tag, wj_type, _error_type) = match display {
64        DisplayStyle::Block => ("div", "wj-math-block", "wj-error-block"),
65        DisplayStyle::Inline => ("span", "wj-math-inline", "wj-error-inline"),
66    };
67
68    // Outer container
69    ctx.html()
70        .tag(html_tag)
71        .attr(attr!(
72            "class" => "wj-math " wj_type,
73            "data-name" => name.unwrap_or(""); if name.is_some(),
74        ))
75        .inner(|ctx| {
76            // Add equation index
77            if let Some(index) = index {
78                ctx.html()
79                    .span()
80                    .attr(attr!("class" => "wj-equation-number"))
81                    .inner(|ctx| {
82                        // Open parenthesis
83                        ctx.html()
84                            .span()
85                            .attr(attr!(
86                                "class" => "wj-equation-paren wj-equation-paren-open",
87                            ))
88                            .contents("(");
89
90                        str_write!(ctx, "{index}");
91
92                        // Close parenthesis
93                        ctx.html()
94                            .span()
95                            .attr(attr!(
96                                "class" => "wj-equation-paren wj-equation-paren-close",
97                            ))
98                            .contents(")");
99                    });
100            }
101
102            // Add LaTeX source (hidden)
103            // Can't use a pre tag because that won't work for inline tags
104            ctx.html()
105                .code()
106                .attr(attr!(
107                    "class" => "wj-math-source wj-hidden",
108                    "aria-hidden" => "true",
109                ))
110                .contents(latex_source);
111
112            // Add generated MathML
113            cfg_if! {
114                if #[cfg(feature = "mathml")] {
115                    match latex_to_mathml(latex_source, display) {
116                        Ok(mathml) => {
117                            debug!("Processed LaTeX -> MathML");
118
119                            // Inject MathML elements
120                            ctx.html()
121                                .element("wj-math-ml")
122                                .attr(attr!("class" => "wj-math-ml"))
123                                .inner(|ctx| ctx.push_raw_str(&mathml));
124                        }
125                        Err(error) => {
126                            warn!("Error processing LaTeX -> MathML: {error}");
127                            let error = str!(error);
128
129                            ctx.html()
130                                .span()
131                                .attr(attr!("class" => _error_type))
132                                .contents(error);
133                        }
134                    }
135                }
136            }
137        });
138}
139
140pub fn render_equation_reference(ctx: &mut HtmlContext, name: &str) {
141    debug!("Rendering equation reference (name '{name}')");
142
143    ctx.html()
144        .span()
145        .attr(attr!("class" => "wj-equation-ref"))
146        .inner(|ctx| {
147            // Equation marker that is hoverable
148            ctx.html()
149                .element("wj-equation-ref-marker")
150                .attr(attr!(
151                    "class" => "wj-equation-ref-marker",
152                    "type" => "button",
153                    "data-name" => name,
154                ))
155                .contents(name);
156
157            // Tooltip shown on hover.
158            ctx.html().span().attr(attr!(
159                "class" => "wj-equation-ref-tooltip",
160                "aria-hidden" => "true",
161            ));
162            // TODO tooltip contents
163        });
164}