Skip to main content

katex_parser/
settings.rs

1use std::collections::HashMap;
2use std::rc::Rc;
3
4use crate::error::ParseError;
5use crate::macro_definition::MacroDefinition;
6use crate::source_location::SourceLocation;
7use crate::token::{token_location, Token};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10/// The result of a strict-mode check: ignore, warn, or error.
11pub enum StrictResponse {
12    Ignore,
13    Warn,
14    Error,
15}
16
17pub type StrictHandler =
18    Rc<dyn Fn(&str, &str, Option<&SourceLocation>) -> Result<StrictResponse, ParseError>>;
19
20/// The strictness policy for LaTeX-incompatible input.
21pub enum Strictness {
22    Ignore,
23    Warn,
24    Error,
25    Callback(StrictHandler),
26}
27
28impl Clone for Strictness {
29    fn clone(&self) -> Self {
30        match self {
31            Strictness::Ignore => Strictness::Ignore,
32            Strictness::Warn => Strictness::Warn,
33            Strictness::Error => Strictness::Error,
34            Strictness::Callback(handler) => Strictness::Callback(handler.clone()),
35        }
36    }
37}
38
39/// A callback invoked when strict mode produces a warning.
40pub type StrictWarningHandler = Rc<dyn Fn(&str)>;
41
42/// The context of a trust check for potentially unsafe commands.
43pub enum TrustContext {
44    UrlTrust {
45        command: String,
46        url: String,
47        protocol: Option<String>,
48    },
49    HtmlClass {
50        class: String,
51    },
52    HtmlId {
53        id: String,
54    },
55    HtmlStyle {
56        style: String,
57    },
58    HtmlData {
59        attributes: HashMap<String, String>,
60    },
61}
62
63/// A callback deciding whether a trust context is accepted.
64pub type TrustHandler = Rc<dyn Fn(&TrustContext) -> bool>;
65
66/// The trust policy for commands that could be unsafe (\href, \url, HTML).
67pub enum TrustPolicy {
68    Untrusted,
69    Trusted,
70    Callback(TrustHandler),
71}
72
73impl Clone for TrustPolicy {
74    fn clone(&self) -> Self {
75        match self {
76            TrustPolicy::Untrusted => TrustPolicy::Untrusted,
77            TrustPolicy::Trusted => TrustPolicy::Trusted,
78            TrustPolicy::Callback(handler) => TrustPolicy::Callback(handler.clone()),
79        }
80    }
81}
82
83#[derive(Debug, Clone)]
84/// A persistent macro store shared across parses.
85pub struct Macros(pub HashMap<String, MacroDefinition>);
86
87impl Macros {
88    pub fn new(macros: HashMap<String, String>) -> Self {
89        let definitions = macros
90            .into_iter()
91            .map(|(name, expansion)| (name, MacroDefinition::text(expansion)))
92            .collect();
93        Macros(definitions)
94    }
95}
96
97#[derive(Clone)]
98/// Parser configuration: display mode, macros, strictness, trust, and limits.
99pub struct Settings {
100    pub throw_on_error: bool,
101    pub display_mode: bool,
102    pub leqno: bool,
103    pub error_color: String,
104    pub color_is_text_color: bool,
105    pub max_expand: usize,
106    pub global_group: bool,
107    pub macros: HashMap<String, String>,
108    pub macro_store: Option<Macros>,
109    pub strict: Strictness,
110    pub strict_warning_handler: Option<StrictWarningHandler>,
111    pub trust: TrustPolicy,
112}
113
114impl Settings {
115    pub fn new() -> Self {
116        Settings {
117            throw_on_error: true,
118            display_mode: false,
119            leqno: false,
120            error_color: "#cc0000".to_string(),
121            color_is_text_color: false,
122            max_expand: 1000,
123            global_group: false,
124            macros: HashMap::new(),
125            macro_store: None,
126            strict: Strictness::Ignore,
127            strict_warning_handler: None,
128            trust: TrustPolicy::Untrusted,
129        }
130    }
131
132    pub fn macro_definitions(&self) -> HashMap<String, MacroDefinition> {
133        self.macros
134            .iter()
135            .map(|(name, expansion)| (name.clone(), MacroDefinition::text(expansion.clone())))
136            .collect()
137    }
138
139    pub fn use_strict_behavior(
140        &self,
141        error_code: &str,
142        error_message: &str,
143        token: Option<&Token>,
144    ) -> bool {
145        match strict_response(&self.strict, error_code, error_message, token_location(token)) {
146            StrictResponse::Error => true,
147            StrictResponse::Warn => {
148                let warning = format!(
149                    "LaTeX-incompatible input and strict mode is set to 'warn': {error_message} [{error_code}]"
150                );
151                if let Some(handler) = &self.strict_warning_handler {
152                    handler(&warning);
153                }
154                false
155            }
156            StrictResponse::Ignore => false,
157        }
158    }
159
160    pub fn report_nonstrict(
161        &self,
162        error_code: &str,
163        error_message: &str,
164        token: Option<&Token>,
165    ) -> Result<(), ParseError> {
166        match strict_response(&self.strict, error_code, error_message, token_location(token)) {
167            StrictResponse::Ignore => Ok(()),
168            StrictResponse::Warn => {
169                let warning = format!(
170                    "LaTeX-incompatible input and strict mode is set to 'warn': {error_message} [{error_code}]"
171                );
172                if let Some(handler) = &self.strict_warning_handler {
173                    handler(&warning);
174                }
175                Ok(())
176            }
177            StrictResponse::Error => Err(ParseError::InvalidArgument {
178                message: format!(
179                    "LaTeX-incompatible input and strict mode is set to 'error': {error_message} [{error_code}]"
180                ),
181                loc: token_location(token),
182            }),
183        }
184    }
185
186    pub fn is_trusted(&self, context: TrustContext) -> bool {
187        let context = if let TrustContext::UrlTrust { command, url, .. } = context {
188            let Some(protocol) = url_protocol(&url) else {
189                return false;
190            };
191            TrustContext::UrlTrust {
192                command,
193                url,
194                protocol: Some(protocol),
195            }
196        } else {
197            context
198        };
199        match &self.trust {
200            TrustPolicy::Untrusted => false,
201            TrustPolicy::Trusted => true,
202            TrustPolicy::Callback(handler) => handler(&context),
203        }
204    }
205}
206
207impl Default for Settings {
208    fn default() -> Self {
209        Settings::new()
210    }
211}
212
213fn strict_response(
214    strictness: &Strictness,
215    error_code: &str,
216    error_message: &str,
217    loc: Option<SourceLocation>,
218) -> StrictResponse {
219    match strictness {
220        Strictness::Ignore => StrictResponse::Ignore,
221        Strictness::Warn => StrictResponse::Warn,
222        Strictness::Error => StrictResponse::Error,
223        Strictness::Callback(handler) => {
224            handler(error_code, error_message, loc.as_ref()).unwrap_or(StrictResponse::Error)
225        }
226    }
227}
228
229fn ascii_lower(text: &str) -> String {
230    text.chars().map(|c| c.to_ascii_lowercase()).collect()
231}
232
233fn url_starts_with(url: &[char], offset: usize, prefix: &str) -> bool {
234    let prefix: Vec<char> = prefix.chars().collect();
235    let prefix_len = prefix.len();
236    offset + prefix_len <= url.len() && url[offset..offset + prefix_len] == prefix[..]
237}
238
239fn encoded_colon_at(url: &[char], offset: usize) -> bool {
240    let lower: Vec<char> = url.iter().map(|c| c.to_ascii_lowercase()).collect();
241    if url_starts_with(&lower, offset, "&colon") {
242        return true;
243    }
244    if !url_starts_with(url, offset, "&#") {
245        return false;
246    }
247    let mut index = offset + 2;
248    if index < url.len() && (url[index] == 'x' || url[index] == 'X') {
249        index += 1;
250        while index < url.len() && url[index] == '0' {
251            index += 1;
252        }
253        url_starts_with(&lower, index, "3a")
254    } else {
255        while index < url.len() && url[index] == '0' {
256            index += 1;
257        }
258        url_starts_with(url, index, "58")
259    }
260}
261
262fn url_protocol(url: &str) -> Option<String> {
263    let chars: Vec<char> = url.chars().collect();
264    let len = chars.len();
265    let mut index = 0;
266    while index < len && (chars[index] as u32) <= 0x20 {
267        index += 1;
268    }
269    let start = index;
270    while index < len {
271        let code = chars[index];
272        if code == ':' {
273            if index <= start {
274                return None;
275            }
276            let scheme: String = chars[start..index].iter().collect();
277            if !scheme.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
278                return None;
279            }
280            for c in scheme.chars() {
281                if !(c.is_ascii_alphabetic()
282                    || c.is_ascii_digit()
283                    || c == '+'
284                    || c == '-'
285                    || c == '.')
286                {
287                    return None;
288                }
289            }
290            return Some(ascii_lower(&scheme));
291        }
292        if code == '/' || code == '#' || code == '?' {
293            return Some("_relative".to_string());
294        }
295        if code == '&' && encoded_colon_at(&chars, index) {
296            return None;
297        }
298        index += 1;
299    }
300    Some("_relative".to_string())
301}