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)]
10pub 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
20pub 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
39pub type StrictWarningHandler = Rc<dyn Fn(&str)>;
41
42pub 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
63pub type TrustHandler = Rc<dyn Fn(&TrustContext) -> bool>;
65
66pub 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)]
84pub 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)]
98pub 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}