1use std::collections::{HashMap, HashSet};
7
8use smol_str::SmolStr;
9
10use crate::semantic::signature::{ArgKind, ArgSpec, EnvironmentSig, builtin};
11use crate::syntax::SyntaxKind;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Token {
16 pub kind: SyntaxKind,
17 pub text: SmolStr,
18}
19
20#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
25pub enum LatexFlavor {
26 #[default]
28 Document,
29 Package,
31}
32
33impl LatexFlavor {
34 fn letter_mode_start(self) -> bool {
37 matches!(self, LatexFlavor::Package)
38 }
39}
40
41#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
52pub struct LexConfig {
53 pub flavor: LatexFlavor,
55 pub dtx: bool,
57}
58
59impl From<LatexFlavor> for LexConfig {
60 fn from(flavor: LatexFlavor) -> Self {
63 Self { flavor, dtx: false }
64 }
65}
66
67#[derive(Debug, Default, Clone, PartialEq, Eq)]
69pub struct ParseCtx {
70 commands: HashMap<SmolStr, Vec<ArgSpec>>,
71 environments: HashMap<SmolStr, Vec<ArgSpec>>,
72 suppressed: HashSet<SmolStr>,
73 begin_aliases: HashMap<SmolStr, SmolStr>,
74 end_aliases: HashMap<SmolStr, SmolStr>,
75 declared_environments: HashMap<SmolStr, EnvironmentSig>,
76}
77
78pub type VerbCtx = ParseCtx;
80
81impl ParseCtx {
82 pub fn is_empty(&self) -> bool {
84 self.commands.is_empty()
85 && self.environments.is_empty()
86 && self.suppressed.is_empty()
87 && self.begin_aliases.is_empty()
88 && self.end_aliases.is_empty()
89 && self.declared_environments.is_empty()
90 }
91
92 pub fn overlay_declarations(&mut self, declared: &crate::declarations::ResolvedDeclarations) {
94 let db = declared.as_db();
95 for name in db.environment_names() {
96 if let Some(sig) = db.environment(name) {
97 self.declared_environments
98 .insert(SmolStr::new(name), sig.clone());
99 }
100 }
101 for (name, target) in db.env_begin_aliases() {
102 self.insert_begin_alias(SmolStr::new(name), SmolStr::new(target));
103 }
104 for (name, target) in db.env_end_aliases() {
105 self.insert_end_alias(SmolStr::new(name), SmolStr::new(target));
106 }
107 }
108
109 pub(crate) fn insert(&mut self, name: SmolStr, leading: Vec<ArgSpec>) {
110 self.commands.insert(name, leading);
111 }
112
113 pub(crate) fn suppress(&mut self, name: SmolStr) {
114 self.suppressed.insert(name);
115 }
116
117 fn is_suppressed(&self, name: &str) -> bool {
118 self.suppressed.contains(name)
119 }
120
121 pub(crate) fn insert_environment(&mut self, name: SmolStr, args: Vec<ArgSpec>) {
122 self.environments.insert(name, args);
123 }
124
125 fn leading_args(&self, name: &str) -> Option<&[ArgSpec]> {
126 self.commands.get(name).map(Vec::as_slice)
127 }
128
129 fn verbatim_environment_args(&self, name: &str) -> Option<&[ArgSpec]> {
130 if let Some(sig) = self.declared_environment(name) {
131 return sig.verbatim_body.then(|| &*sig.args);
132 }
133 self.environments.get(name).map(Vec::as_slice)
134 }
135
136 pub(crate) fn is_verbatim_environment(&self, name: &str) -> bool {
137 match self.declared_environment(name) {
138 Some(sig) => sig.verbatim_body,
139 None => {
140 self.environments.contains_key(name)
141 || builtin()
142 .environment(name)
143 .is_some_and(|env| env.verbatim_body)
144 }
145 }
146 }
147
148 pub(crate) fn insert_begin_alias(&mut self, name: SmolStr, target: SmolStr) {
149 self.begin_aliases.insert(name, target);
150 }
151
152 pub(crate) fn insert_end_alias(&mut self, name: SmolStr, target: SmolStr) {
153 self.end_aliases.insert(name, target);
154 }
155
156 pub(crate) fn begin_alias(&self, name: &str) -> Option<&str> {
157 self.begin_aliases.get(name).map(SmolStr::as_str)
158 }
159
160 pub(crate) fn end_alias(&self, name: &str) -> Option<&str> {
161 self.end_aliases.get(name).map(SmolStr::as_str)
162 }
163
164 pub(crate) fn begin_alias_targets(&self) -> impl Iterator<Item = &str> {
165 self.begin_aliases.values().map(SmolStr::as_str)
166 }
167
168 fn declared_environment(&self, name: &str) -> Option<&EnvironmentSig> {
169 self.declared_environments.get(name)
170 }
171
172 pub(crate) fn is_block_environment(&self, name: &str) -> bool {
173 match self.declared_environment(name) {
174 Some(sig) => sig.block(),
175 None => builtin()
176 .environment(name)
177 .is_some_and(EnvironmentSig::block),
178 }
179 }
180
181 pub(crate) fn is_math_environment(&self, name: &str) -> bool {
182 match self.declared_environment(name) {
183 Some(sig) => sig.math,
184 None => builtin().environment(name).is_some_and(|env| env.math),
185 }
186 }
187
188 pub(crate) fn is_statement_environment(&self, name: &str) -> bool {
189 match self.declared_environment(name) {
190 Some(sig) => sig.statement_body,
191 None => builtin()
192 .environment(name)
193 .is_some_and(|env| env.statement_body),
194 }
195 }
196
197 pub(crate) fn has_env_aliases(&self) -> bool {
198 !self.begin_aliases.is_empty() || !self.end_aliases.is_empty()
199 }
200}
201
202pub(crate) fn is_definition_keyword(text: &str) -> bool {
204 matches!(
205 text,
206 "\\newcommand"
207 | "\\renewcommand"
208 | "\\providecommand"
209 | "\\DeclareRobustCommand"
210 | "\\NewDocumentCommand"
211 | "\\RenewDocumentCommand"
212 | "\\ProvideDocumentCommand"
213 | "\\DeclareDocumentCommand"
214 | "\\def"
215 | "\\edef"
216 | "\\gdef"
217 | "\\xdef"
218 | "\\let"
219 )
220}
221
222pub(crate) fn definition_name_slots(text: &str) -> u8 {
224 match text {
225 "\\let" => 2,
226 _ if is_definition_keyword(text) => 1,
227 _ => 0,
228 }
229}
230
231fn is_literal_token_command(text: &str) -> bool {
232 matches!(
233 text,
234 "\\string" | "\\noexpand" | "\\meaning" | "\\expandafter" | "\\show"
235 )
236}
237
238fn is_char_constant_command(text: &str) -> bool {
239 matches!(
240 text,
241 "\\char"
242 | "\\catcode"
243 | "\\lccode"
244 | "\\uccode"
245 | "\\sfcode"
246 | "\\mathcode"
247 | "\\delcode"
248 | "\\number"
249 | "\\the"
250 | "\\romannumeral"
251 | "\\numexpr"
252 | "\\dimexpr"
253 | "\\ifnum"
254 | "\\ifodd"
255 | "\\ifdim"
256 )
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum ExplToggle {
262 On,
263 Off,
264}
265
266pub fn expl_toggle(text: &str) -> Option<ExplToggle> {
268 match text {
269 "\\ExplSyntaxOn"
270 | "\\ProvidesExplPackage"
271 | "\\ProvidesExplClass"
272 | "\\ProvidesExplFile" => Some(ExplToggle::On),
273 "\\ExplSyntaxOff" => Some(ExplToggle::Off),
274 _ => None,
275 }
276}
277
278pub(crate) fn dtx_has_expl_signal(input: &str) -> bool {
279 input.contains("\\ProvidesExpl")
280 || input
281 .lines()
282 .any(|l| l.starts_with("%<@@=") && l[5..].contains('>'))
283}
284
285pub fn lex(input: &str) -> Vec<Token> {
287 lex_with(input, &ParseCtx::default(), LexConfig::default())
288}
289
290pub fn lex_with(input: &str, ctx: &ParseCtx, config: LexConfig) -> Vec<Token> {
292 Lexer::new(input, ctx, config, None).run()
293}
294
295pub(crate) fn lex_with_implicit_expl(
296 input: &str,
297 ctx: &ParseCtx,
298 config: LexConfig,
299 implicit_expl: bool,
300) -> Vec<Token> {
301 Lexer::new(input, ctx, config, Some(implicit_expl)).run()
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305enum Pending {
306 Delim,
307 Def,
308 CharConstant,
309 LiteralToken,
310}
311
312#[derive(Debug, Clone, Copy)]
313struct MacrocodeSave {
314 at_letter: bool,
315 expl_syntax: bool,
316}
317
318struct Lexer<'a> {
319 input: &'a str,
320 ctx: &'a ParseCtx,
321 config: LexConfig,
322 implicit_expl: bool,
323 out: Vec<Token>,
324 pos: usize,
325 at_letter: bool,
326 expl_syntax: bool,
327 at_line_start: bool,
328 in_doc_line: bool,
329 short_verbs: Vec<char>,
330 macrocode: Option<MacrocodeSave>,
331 pending: Option<Pending>,
332 brace_depth: usize,
333 brace_counted: usize,
334}
335
336impl<'a> Lexer<'a> {
337 fn new(
338 input: &'a str,
339 ctx: &'a ParseCtx,
340 config: LexConfig,
341 implicit_expl_override: Option<bool>,
342 ) -> Self {
343 let implicit_expl = if config.dtx {
344 implicit_expl_override.unwrap_or_else(|| dtx_has_expl_signal(input))
345 } else {
346 false
347 };
348 Self {
349 input,
350 ctx,
351 config,
352 implicit_expl,
353 out: Vec::new(),
354 pos: 0,
355 at_letter: config.flavor.letter_mode_start(),
356 expl_syntax: false,
357 at_line_start: true,
358 in_doc_line: false,
359 short_verbs: if config.dtx { vec!['|'] } else { Vec::new() },
360 macrocode: None,
361 pending: None,
362 brace_depth: 0,
363 brace_counted: 0,
364 }
365 }
366
367 fn run(mut self) -> Vec<Token> {
368 while self.pos < self.input.len() {
369 self.sync_brace_depth();
370 if self.try_macrocode_frame()
371 || self.try_guard()
372 || self.try_doc_margin()
373 || self.try_verbatim_environment()
374 || self.try_verbatim_arg_environment()
375 {
376 continue;
377 }
378 let word_len = control_word_len(self.rest(), self.at_letter, self.expl_syntax);
379 if self.try_verbatim_command(word_len)
380 || self.try_short_verb()
381 || self.try_char_constant()
382 || self.try_doc_comment()
383 {
384 continue;
385 }
386 self.lex_token(word_len);
387 }
388 self.out
389 }
390
391 fn rest(&self) -> &'a str {
392 &self.input[self.pos..]
393 }
394
395 fn push(&mut self, kind: SyntaxKind, text: &str) {
396 self.out.push(Token {
397 kind,
398 text: SmolStr::new(text),
399 });
400 }
401
402 fn consume(&mut self, len: usize) {
403 self.pos += len;
404 self.at_line_start = false;
405 self.pending = None;
406 }
407
408 fn sync_brace_depth(&mut self) {
409 while self.brace_counted < self.out.len() {
410 match self.out[self.brace_counted].kind {
411 SyntaxKind::L_BRACE => self.brace_depth += 1,
412 SyntaxKind::R_BRACE => self.brace_depth = self.brace_depth.saturating_sub(1),
413 _ => {}
414 }
415 self.brace_counted += 1;
416 }
417 }
418
419 fn try_macrocode_frame(&mut self) -> bool {
420 if !(self.config.dtx && self.at_line_start) {
421 return false;
422 }
423 let rest = self.rest();
424 let want_begin = self.macrocode.is_none();
425 let Some(consumed) = lex_macrocode_frame(rest, want_begin, &mut self.out) else {
426 return false;
427 };
428 match self.macrocode.take() {
429 Some(saved) => {
430 self.at_letter = saved.at_letter;
431 self.expl_syntax = saved.expl_syntax;
432 }
433 None => {
434 self.macrocode = Some(MacrocodeSave {
435 at_letter: self.at_letter,
436 expl_syntax: self.expl_syntax,
437 });
438 self.at_letter = true;
439 if self.implicit_expl {
440 self.expl_syntax = true;
441 }
442 }
443 }
444 self.consume(consumed);
445 true
446 }
447
448 fn try_guard(&mut self) -> bool {
449 let rest = self.rest();
450 if !(self.config.dtx && self.at_line_start && rest.starts_with("%<")) {
451 return false;
452 }
453 let Some(rel) = rest[2..].find(['>', '\n', '\r']) else {
454 return false;
455 };
456 if rest.as_bytes()[2 + rel] != b'>' {
457 return false;
458 }
459 let len = 2 + rel + 1;
460 self.push(SyntaxKind::GUARD, &rest[..len]);
461 self.pos += len;
462 self.at_line_start = false;
463 true
464 }
465
466 fn try_doc_margin(&mut self) -> bool {
467 let rest = self.rest();
468 if !(self.config.dtx
469 && self.at_line_start
470 && self.macrocode.is_none()
471 && rest.starts_with('%')
472 && !rest.starts_with("%<"))
473 {
474 return false;
475 }
476 self.push(SyntaxKind::DOC_MARGIN, "%");
477 self.pos += 1;
478 self.at_line_start = false;
479 self.in_doc_line = true;
480 true
481 }
482
483 fn try_verbatim_environment(&mut self) -> bool {
484 let (rest, ctx) = (self.rest(), self.ctx);
485 let Some(consumed) = lex_verbatim_environment(rest, ctx, &mut self.out) else {
486 return false;
487 };
488 self.consume(consumed);
489 true
490 }
491
492 fn try_verbatim_arg_environment(&mut self) -> bool {
493 if self.macrocode.is_some() {
494 return false;
495 }
496 let rest = self.rest();
497 let Some(consumed) = lex_verbatim_arg_environment(rest, &mut self.out) else {
498 return false;
499 };
500 self.consume(consumed);
501 true
502 }
503
504 fn try_verbatim_command(&mut self, word_len: Option<usize>) -> bool {
505 if self.pending == Some(Pending::Def) {
506 return false;
507 }
508 let (rest, ctx) = (self.rest(), self.ctx);
509 let Some(consumed) = lex_verbatim_command(
510 rest,
511 word_len,
512 ctx,
513 self.config.dtx && self.in_doc_line,
514 &mut self.out,
515 ) else {
516 return false;
517 };
518 self.consume(consumed);
519 true
520 }
521
522 fn try_short_verb(&mut self) -> bool {
523 if self.short_verbs.is_empty()
524 || self.macrocode.is_some()
525 || matches!(self.pending, Some(Pending::Delim | Pending::LiteralToken))
526 {
527 return false;
528 }
529 let rest = self.rest();
530 if !rest
531 .chars()
532 .next()
533 .is_some_and(|c| self.short_verbs.contains(&c))
534 {
535 return false;
536 }
537 let Some(len) = delimited_len(rest) else {
538 return false;
539 };
540 self.push(SyntaxKind::VERB, &rest[..len]);
541 self.consume(len);
542 true
543 }
544
545 fn try_char_constant(&mut self) -> bool {
546 let numeric_context = self.pending == Some(Pending::CharConstant);
547 let rest = self.rest();
548 let Some(after) = rest.strip_prefix('`') else {
549 return false;
550 };
551 let Some(c) = after.chars().next() else {
552 return false;
553 };
554 if matches!(c, '\n' | '\r') || (self.brace_depth > 0 && matches!(c, '{' | '}')) {
555 return false;
556 }
557 let len = if c == '\\' {
558 match after[1..]
559 .chars()
560 .next()
561 .filter(|e| !matches!(e, '\n' | '\r'))
562 {
563 Some(e) => 2 + e.len_utf8(),
564 None => return false,
565 }
566 } else {
567 1 + c.len_utf8()
568 };
569 let alignment_cell = self.pending.is_none() && c == '\\' && rest[len..].starts_with('&');
570 if !numeric_context && !alignment_cell {
571 return false;
572 }
573 self.push(SyntaxKind::WORD, &rest[..len]);
574 self.consume(len);
575 true
576 }
577
578 fn try_doc_comment(&mut self) -> bool {
579 let rest = self.rest();
580 if !(self.in_doc_line && rest.starts_with("^^A")) {
581 return false;
582 }
583 let len = run_len(rest, |c| c != '\n' && c != '\r');
584 self.push(SyntaxKind::COMMENT, &rest[..len]);
585 self.consume(len);
586 true
587 }
588
589 fn lex_token(&mut self, word_len: Option<usize>) {
590 let rest = self.rest();
591 let (kind, mut len) = next_token(rest, word_len, self.expl_syntax);
592 if self.pending == Some(Pending::Delim) && kind == SyntaxKind::WORD {
593 len = rest.chars().next().expect("rest is non-empty").len_utf8();
594 }
595 if kind == SyntaxKind::WORD
596 && !self.short_verbs.is_empty()
597 && let Some((i, c)) = rest[..len]
598 .char_indices()
599 .find(|(_, c)| self.short_verbs.contains(c))
600 {
601 len = if i == 0 { c.len_utf8() } else { i };
602 }
603 debug_assert!(len > 0, "lexer made no progress at byte {}", self.pos);
604 let text = &rest[..len];
605 if kind == SyntaxKind::CONTROL_WORD {
606 self.apply_toggles(text, &rest[len..]);
607 }
608 self.pending = next_pending(self.pending, kind, text);
609 self.push(kind, text);
610 self.at_line_start =
611 kind == SyntaxKind::NEWLINE || text.ends_with('\n') || text.ends_with('\r');
612 if self.at_line_start {
613 self.in_doc_line = false;
614 }
615 self.pos += len;
616 }
617
618 fn apply_toggles(&mut self, text: &str, after: &str) {
619 match text {
620 "\\makeatletter" => self.at_letter = true,
621 "\\makeatother" => self.at_letter = false,
622 "\\MakeShortVerb" => {
623 if let Some(c) = short_verb_char(after)
624 && !self.short_verbs.contains(&c)
625 {
626 self.short_verbs.push(c);
627 }
628 }
629 "\\DeleteShortVerb" => {
630 if let Some(c) = short_verb_char(after) {
631 self.short_verbs.retain(|&x| x != c);
632 }
633 }
634 "\\documentclass" | "\\LoadClass" => {
635 if doc_class_enables_bar(after) && !self.short_verbs.contains(&'|') {
636 self.short_verbs.push('|');
637 }
638 }
639 _ => {
640 if let Some(toggle) = expl_toggle(text) {
641 self.expl_syntax = matches!(toggle, ExplToggle::On);
642 }
643 }
644 }
645 }
646}
647
648pub(crate) fn reads_following_text(text: &str) -> bool {
649 matches!(
650 text,
651 "\\MakeShortVerb" | "\\DeleteShortVerb" | "\\documentclass" | "\\LoadClass"
652 ) || next_pending(None, SyntaxKind::CONTROL_WORD, text).is_some()
653}
654
655fn next_pending(pending: Option<Pending>, kind: SyntaxKind, text: &str) -> Option<Pending> {
656 if kind == SyntaxKind::CONTROL_WORD {
657 return if text == "\\left" || text == "\\right" {
658 Some(Pending::Delim)
659 } else if is_definition_keyword(text) {
660 Some(Pending::Def)
661 } else if is_char_constant_command(text) {
662 Some(Pending::CharConstant)
663 } else if is_literal_token_command(text) {
664 Some(Pending::LiteralToken)
665 } else {
666 None
667 };
668 }
669 match pending? {
670 p @ (Pending::Delim | Pending::Def)
671 if matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE) =>
672 {
673 Some(p)
674 }
675 Pending::Def if kind == SyntaxKind::L_BRACE => Some(Pending::Def),
676 p @ (Pending::CharConstant | Pending::LiteralToken) if kind == SyntaxKind::WHITESPACE => {
677 Some(p)
678 }
679 _ => None,
680 }
681}
682
683fn control_word_len(rest: &str, at_letter: bool, expl_syntax: bool) -> Option<usize> {
684 let after = rest.strip_prefix('\\')?;
685 let letters = run_len(after, |c| is_letter(c, at_letter, expl_syntax));
686 (letters > 0).then_some(1 + letters)
687}
688
689fn next_token(rest: &str, word_len: Option<usize>, expl_syntax: bool) -> (SyntaxKind, usize) {
690 let c = rest.chars().next().expect("rest is non-empty");
691 match c {
692 '\\' => lex_control(rest, word_len),
693 '%' => (
694 SyntaxKind::COMMENT,
695 run_len(rest, |c| c != '\n' && c != '\r'),
696 ),
697 '{' => (SyntaxKind::L_BRACE, 1),
698 '}' => (SyntaxKind::R_BRACE, 1),
699 '[' => (SyntaxKind::L_BRACKET, 1),
700 ']' => (SyntaxKind::R_BRACKET, 1),
701 '$' => (SyntaxKind::DOLLAR, 1),
702 '&' => (SyntaxKind::AMPERSAND, 1),
703 '#' => (SyntaxKind::HASH, 1),
704 '^' => (SyntaxKind::CARET, 1),
705 '_' if !expl_syntax => (SyntaxKind::UNDERSCORE, 1),
706 '~' => (SyntaxKind::TILDE, 1),
707 '\n' => (SyntaxKind::NEWLINE, 1),
708 '\r' => {
709 let len = if rest.as_bytes().get(1) == Some(&b'\n') {
710 2
711 } else {
712 1
713 };
714 (SyntaxKind::NEWLINE, len)
715 }
716 ' ' | '\t' => (
717 SyntaxKind::WHITESPACE,
718 run_len(rest, |c| c == ' ' || c == '\t'),
719 ),
720 _ => (
721 SyntaxKind::WORD,
722 run_len(rest, |c| is_word_char(c) || (expl_syntax && c == '_')),
723 ),
724 }
725}
726
727fn lex_control(rest: &str, word_len: Option<usize>) -> (SyntaxKind, usize) {
728 match word_len {
729 Some(word_len) => {
730 if &rest[..word_len] == "\\verb"
731 && let Some(arg_len) = verb_len(&rest[word_len..])
732 {
733 return (SyntaxKind::VERB, word_len + arg_len);
734 }
735 (SyntaxKind::CONTROL_WORD, word_len)
736 }
737 None => {
738 let after = &rest[1..];
739 let symbol_len = if after.starts_with("\r\n") {
740 2
741 } else {
742 after.chars().next().map_or(0, char::len_utf8)
743 };
744 (SyntaxKind::CONTROL_SYMBOL, 1 + symbol_len)
745 }
746 }
747}
748
749fn verb_len(after: &str) -> Option<usize> {
750 match after.strip_prefix('*') {
751 Some(rest) => Some(1 + delimited_len(rest)?),
752 None => delimited_len(after),
753 }
754}
755
756fn delimited_len(after: &str) -> Option<usize> {
757 let mut chars = after.chars();
758 let delim = chars.next()?;
759 if delim.is_whitespace() {
760 return None;
761 }
762 let mut consumed = delim.len_utf8();
763 for c in chars {
764 if c == '\n' || c == '\r' {
765 return None;
766 }
767 consumed += c.len_utf8();
768 if c == delim {
769 return Some(consumed);
770 }
771 }
772 None
773}
774
775fn short_verb_char(after: &str) -> Option<char> {
776 let s = skip_inline_ws(after.strip_prefix('*').unwrap_or(after));
777 let (body, braced) = match s.strip_prefix('{') {
778 Some(inner) => (skip_inline_ws(inner), true),
779 None => (s, false),
780 };
781 let arg = body.strip_prefix('\\')?;
782 let c = arg.chars().next()?;
783 if c == '\n' || c == '\r' {
784 return None;
785 }
786 if braced && !skip_inline_ws(&arg[c.len_utf8()..]).starts_with('}') {
787 return None;
788 }
789 Some(c)
790}
791
792const BAR_SHORT_VERB_CLASSES: [&str; 5] = ["ltxdoc", "ltxguide", "ltnews", "l3doc", "amsldoc"];
793
794fn doc_class_enables_bar(after: &str) -> bool {
795 let mut s = skip_inline_ws(after);
796 if let Some(rest) = s.strip_prefix('[') {
797 match rest.find(']') {
798 Some(i) => s = rest[i + 1..].trim_start_matches([' ', '\t', '\n', '\r']),
799 None => return false,
800 }
801 }
802 let Some(rest) = s.strip_prefix('{') else {
803 return false;
804 };
805 let Some(close) = rest.find('}') else {
806 return false;
807 };
808 BAR_SHORT_VERB_CLASSES.contains(&rest[..close].trim())
809}
810
811fn lex_verbatim_environment(rest: &str, ctx: &ParseCtx, out: &mut Vec<Token>) -> Option<usize> {
812 let (name, prefix_len) = begin_name(rest)?;
813 let args: &[ArgSpec] = match ctx.verbatim_environment_args(name) {
814 Some(args) => args,
815 None => {
816 &builtin()
817 .environment(name)
818 .filter(|e| e.verbatim_body)?
819 .args
820 }
821 };
822
823 push_env_delimiter(out, "\\begin", name);
824
825 let args_region = &rest[prefix_len..];
826 let args_len = scan_verbatim_args(args_region, args);
827 lex_into(&args_region[..args_len], out);
828
829 let body_region = &args_region[args_len..];
830 let body_len = verbatim_body_len(body_region, name);
831 if body_len > 0 {
832 out.push(Token {
833 kind: SyntaxKind::VERBATIM_BODY,
834 text: SmolStr::new(&body_region[..body_len]),
835 });
836 }
837 Some(prefix_len + args_len + body_len)
838}
839
840fn verbatim_body_len(body: &str, name: &str) -> usize {
841 const LEAD: &str = "\\end{";
842 let mut from = 0;
843 while let Some(rel) = body[from..].find(LEAD) {
844 let at = from + rel;
845 let after = &body[at + LEAD.len()..];
846 if let Some(tail) = after.strip_prefix(name)
847 && tail.starts_with('}')
848 {
849 return at;
850 }
851 from = at + LEAD.len();
852 }
853 body.len()
854}
855
856fn lex_verbatim_arg_environment(rest: &str, out: &mut Vec<Token>) -> Option<usize> {
857 let (name, prefix_len) = begin_name(rest)?;
858 builtin().environment(name).filter(|e| e.verbatim_arg)?;
859
860 let region = &rest[prefix_len..];
861 let mut args_len = 0;
862 if let Some(after) = region.strip_prefix('[') {
863 let i = after.find([']', '\n', '\r'])?;
864 if after.as_bytes()[i] != b']' {
865 return None;
866 }
867 args_len = 1 + i + 1;
868 }
869 let arg_region = ®ion[args_len..];
870 let delim = arg_region.chars().next()?;
871 let braced_content_len = if delim == '{' {
872 Some(braced_verb_content_len(&arg_region[1..])?)
873 } else {
874 if !delim.is_ascii_punctuation()
875 || matches!(delim, '\\' | '}' | '[' | ']' | '%' | '*' | '$')
876 {
877 return None;
878 }
879 None
880 };
881
882 push_env_delimiter(out, "\\begin", name);
883 lex_into(®ion[..args_len], out);
884 let verb_len = match braced_content_len {
885 Some(content_len) => {
886 out.push(Token {
887 kind: SyntaxKind::L_BRACE,
888 text: SmolStr::new("{"),
889 });
890 out.push(Token {
891 kind: SyntaxKind::VERB,
892 text: SmolStr::new(&arg_region[1..1 + content_len]),
893 });
894 out.push(Token {
895 kind: SyntaxKind::R_BRACE,
896 text: SmolStr::new("}"),
897 });
898 1 + content_len + 1
899 }
900 None => {
901 let verb_len = delimited_len(arg_region)?;
902 out.push(Token {
903 kind: SyntaxKind::VERB,
904 text: SmolStr::new(&arg_region[..verb_len]),
905 });
906 verb_len
907 }
908 };
909 Some(prefix_len + args_len + verb_len)
910}
911
912fn braced_verb_content_len(content: &str) -> Option<usize> {
913 let mut depth = 1usize;
914 let mut chars = content.char_indices();
915 while let Some((i, c)) = chars.next() {
916 match c {
917 '\\' => {
918 chars.next()?;
919 }
920 '{' => depth += 1,
921 '}' => {
922 depth -= 1;
923 if depth == 0 {
924 return (i > 0).then_some(i);
925 }
926 }
927 '\n' | '\r' => return None,
928 _ => {}
929 }
930 }
931 None
932}
933
934fn lex_macrocode_frame(rest: &str, want_begin: bool, out: &mut Vec<Token>) -> Option<usize> {
935 let indent = if want_begin { inline_ws_len(rest) } else { 0 };
936 let after_pct = rest[indent..].strip_prefix('%')?;
937 let ws_len = inline_ws_len(after_pct);
938 let body = &after_pct[ws_len..];
939 let (control, open) = if want_begin {
940 ("\\begin", "\\begin{")
941 } else {
942 ("\\end", "\\end{")
943 };
944 let after_open = body.strip_prefix(open)?;
945 let close = after_open.find('}')?;
946 let name = &after_open[..close];
947 if name != "macrocode" && name != "macrocode*" {
948 return None;
949 }
950 let after_close = &after_open[close + 1..];
951 let tail = skip_inline_ws(after_close);
952 let comment_tail = !want_begin && tail.starts_with('%');
953 if !(tail.is_empty() || tail.starts_with('\n') || tail.starts_with('\r') || comment_tail) {
954 return None;
955 }
956
957 if indent > 0 {
958 out.push(Token {
959 kind: SyntaxKind::WHITESPACE,
960 text: SmolStr::new(&rest[..indent]),
961 });
962 }
963 out.push(Token {
964 kind: SyntaxKind::DOC_MARGIN,
965 text: SmolStr::new("%"),
966 });
967 if ws_len > 0 {
968 out.push(Token {
969 kind: SyntaxKind::WHITESPACE,
970 text: SmolStr::new(&after_pct[..ws_len]),
971 });
972 }
973 push_env_delimiter(out, control, name);
974 Some(indent + 1 + ws_len + control.len() + 1 + name.len() + 1)
975}
976
977fn lex_verbatim_command(
978 rest: &str,
979 word_len: Option<usize>,
980 ctx: &ParseCtx,
981 on_dtx_doc_line: bool,
982 out: &mut Vec<Token>,
983) -> Option<usize> {
984 let word_len = word_len?;
985 let name = &rest[1..word_len];
986 if name == "verb" {
987 return None;
988 }
989 let (leading, delimited): (&[ArgSpec], bool) = match ctx.leading_args(name) {
990 Some(args) => (args, false),
991 None => {
992 if ctx.is_suppressed(name) {
993 return None;
994 }
995 let sig = builtin().command(name)?;
996 if sig.verbatim {
997 (&sig.args, sig.verbatim_delimited)
998 } else {
999 let raw = sig.args.iter().position(|arg| arg.verbatim)?;
1000 if sig.args[raw].kind != ArgKind::Brace {
1001 return None;
1002 }
1003 (&sig.args[..raw], false)
1004 }
1005 }
1006 };
1007
1008 let after_word = &rest[word_len..];
1009 let args_len = scan_verbatim_args(after_word, leading);
1010
1011 let region = &after_word[args_len..];
1012 let dtx_gap = (!delimited && on_dtx_doc_line)
1013 .then(|| dtx_doc_argument_gap_len(region))
1014 .flatten();
1015 let ws_len = if let Some(len) = dtx_gap {
1016 len
1017 } else if delimited {
1018 inline_ws_len(region)
1019 } else {
1020 tex_whitespace_len(region)
1021 };
1022 let arg_region = ®ion[ws_len..];
1023 let arg_len = match arg_region.bytes().next() {
1024 Some(b'{') => balanced_group_len(arg_region, b'}')?,
1025 Some(_) if delimited => delimited_len(arg_region)?,
1026 _ => return None,
1027 };
1028
1029 out.push(Token {
1030 kind: SyntaxKind::CONTROL_WORD,
1031 text: SmolStr::new(&rest[..word_len]),
1032 });
1033 lex_into(&after_word[..args_len], out);
1034 if ws_len > 0 {
1035 if dtx_gap.is_some() {
1036 lex_dtx_doc_argument_gap(®ion[..ws_len], out);
1037 } else {
1038 out.push(Token {
1039 kind: SyntaxKind::WHITESPACE,
1040 text: SmolStr::new(®ion[..ws_len]),
1041 });
1042 }
1043 }
1044 out.push(Token {
1045 kind: SyntaxKind::VERB,
1046 text: SmolStr::new(&arg_region[..arg_len]),
1047 });
1048 Some(word_len + args_len + ws_len + arg_len)
1049}
1050
1051fn scan_verbatim_args(region: &str, args: &[ArgSpec]) -> usize {
1052 let bytes = region.as_bytes();
1053 let mut pos = 0;
1054 for arg in args {
1055 let probe = pos + inline_ws_len(®ion[pos..]);
1056 let (open, close) = match arg.kind {
1057 ArgKind::Bracket => (b'[', b']'),
1058 ArgKind::Brace => (b'{', b'}'),
1059 };
1060 if bytes.get(probe) != Some(&open) {
1061 continue;
1062 }
1063 match balanced_group_len(®ion[probe..], close) {
1064 Some(len) => pos = probe + len,
1065 None => break, }
1067 }
1068 pos
1069}
1070
1071fn balanced_group_len(s: &str, close: u8) -> Option<usize> {
1072 let bytes = s.as_bytes();
1073 let mut stack = vec![close];
1074 let mut i = 1;
1075 while i < bytes.len() {
1076 match bytes[i] {
1077 b'\\' => {
1078 i += 2;
1079 continue;
1080 }
1081 b'{' => stack.push(b'}'),
1082 b'[' => stack.push(b']'),
1083 c @ (b'}' | b']') if stack.last() == Some(&c) => {
1084 stack.pop();
1085 if stack.is_empty() {
1086 return Some(i + 1);
1087 }
1088 }
1089 _ => {}
1090 }
1091 i += 1;
1092 }
1093 None
1094}
1095
1096fn lex_into(region: &str, out: &mut Vec<Token>) {
1097 let mut pos = 0;
1098 while pos < region.len() {
1099 let rest = ®ion[pos..];
1100 let (kind, len) = next_token(rest, control_word_len(rest, false, false), false);
1101 debug_assert!(len > 0, "lexer made no progress in verbatim args");
1102 out.push(Token {
1103 kind,
1104 text: SmolStr::new(®ion[pos..pos + len]),
1105 });
1106 pos += len;
1107 }
1108}
1109
1110fn begin_name(rest: &str) -> Option<(&str, usize)> {
1111 let after = rest.strip_prefix("\\begin{")?;
1112 let close = after.find('}')?;
1113 Some((&after[..close], "\\begin{".len() + close + 1))
1114}
1115
1116fn push_env_delimiter(out: &mut Vec<Token>, control: &str, name: &str) {
1117 out.push(Token {
1118 kind: SyntaxKind::CONTROL_WORD,
1119 text: SmolStr::new(control),
1120 });
1121 out.push(Token {
1122 kind: SyntaxKind::L_BRACE,
1123 text: SmolStr::new("{"),
1124 });
1125 out.push(Token {
1126 kind: SyntaxKind::WORD,
1127 text: SmolStr::new(name),
1128 });
1129 out.push(Token {
1130 kind: SyntaxKind::R_BRACE,
1131 text: SmolStr::new("}"),
1132 });
1133}
1134
1135fn inline_ws_len(s: &str) -> usize {
1136 s.bytes().take_while(|&b| b == b' ' || b == b'\t').count()
1137}
1138
1139fn tex_whitespace_len(s: &str) -> usize {
1140 s.bytes()
1141 .take_while(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
1142 .count()
1143}
1144
1145fn dtx_doc_argument_gap_len(s: &str) -> Option<usize> {
1146 let inline = inline_ws_len(s);
1147 let rest = &s[inline..];
1148 let newline = if rest.starts_with("\r\n") {
1149 2
1150 } else if rest.starts_with(['\n', '\r']) {
1151 1
1152 } else {
1153 return None;
1154 };
1155 let after_newline = &rest[newline..];
1156 let after_margin = after_newline.strip_prefix('%')?;
1157 Some(inline + newline + 1 + inline_ws_len(after_margin))
1158}
1159
1160fn lex_dtx_doc_argument_gap(gap: &str, out: &mut Vec<Token>) {
1161 let inline = inline_ws_len(gap);
1162 if inline > 0 {
1163 out.push(Token {
1164 kind: SyntaxKind::WHITESPACE,
1165 text: SmolStr::new(&gap[..inline]),
1166 });
1167 }
1168 let rest = &gap[inline..];
1169 let newline = if rest.starts_with("\r\n") { 2 } else { 1 };
1170 out.push(Token {
1171 kind: SyntaxKind::NEWLINE,
1172 text: SmolStr::new(&rest[..newline]),
1173 });
1174 out.push(Token {
1175 kind: SyntaxKind::DOC_MARGIN,
1176 text: SmolStr::new("%"),
1177 });
1178 let trailing = &rest[newline + 1..];
1179 if !trailing.is_empty() {
1180 out.push(Token {
1181 kind: SyntaxKind::WHITESPACE,
1182 text: SmolStr::new(trailing),
1183 });
1184 }
1185}
1186
1187fn skip_inline_ws(s: &str) -> &str {
1188 &s[inline_ws_len(s)..]
1189}
1190
1191fn run_len(s: &str, pred: impl Fn(char) -> bool) -> usize {
1192 let mut len = 0;
1193 for c in s.chars() {
1194 if pred(c) {
1195 len += c.len_utf8();
1196 } else {
1197 break;
1198 }
1199 }
1200 len
1201}
1202
1203fn is_letter(c: char, at_letter: bool, expl_syntax: bool) -> bool {
1204 c.is_ascii_alphabetic() || (at_letter && c == '@') || (expl_syntax && (c == '_' || c == ':'))
1205}
1206
1207pub fn is_control_word_name(name: &str) -> bool {
1209 !name.is_empty() && name.chars().all(|c| is_letter(c, true, true))
1210}
1211
1212pub fn is_word_char(c: char) -> bool {
1214 !matches!(
1215 c,
1216 '\\' | '%'
1217 | '{'
1218 | '}'
1219 | '['
1220 | ']'
1221 | '$'
1222 | '&'
1223 | '#'
1224 | '^'
1225 | '_'
1226 | '~'
1227 | ' '
1228 | '\t'
1229 | '\n'
1230 | '\r'
1231 )
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236 use super::*;
1237
1238 fn assert_lossless(input: &str) {
1239 let joined: String = lex(input).iter().map(|t| t.text.as_str()).collect();
1240 assert_eq!(joined, input);
1241 }
1242
1243 #[test]
1244 fn the_pending_arming_sets_are_disjoint() {
1245 for name in [
1246 "\\left",
1247 "\\right",
1248 "\\newcommand",
1249 "\\renewcommand",
1250 "\\providecommand",
1251 "\\DeclareRobustCommand",
1252 "\\NewDocumentCommand",
1253 "\\RenewDocumentCommand",
1254 "\\ProvideDocumentCommand",
1255 "\\DeclareDocumentCommand",
1256 "\\def",
1257 "\\edef",
1258 "\\gdef",
1259 "\\xdef",
1260 "\\let",
1261 "\\char",
1262 "\\catcode",
1263 "\\lccode",
1264 "\\uccode",
1265 "\\sfcode",
1266 "\\mathcode",
1267 "\\delcode",
1268 "\\number",
1269 "\\the",
1270 "\\romannumeral",
1271 "\\numexpr",
1272 "\\dimexpr",
1273 "\\ifnum",
1274 "\\ifodd",
1275 "\\ifdim",
1276 "\\string",
1277 "\\noexpand",
1278 "\\meaning",
1279 "\\expandafter",
1280 "\\show",
1281 ] {
1282 let armed = [
1283 name == "\\left" || name == "\\right",
1284 is_definition_keyword(name),
1285 is_char_constant_command(name),
1286 is_literal_token_command(name),
1287 ];
1288 assert_eq!(
1289 armed.iter().filter(|&&x| x).count(),
1290 1,
1291 "{name} arms {armed:?} — the `Pending` slot needs disjoint sets"
1292 );
1293 }
1294 }
1295
1296 #[test]
1297 fn a_claimed_construct_spends_the_armed_char_constant_mode() {
1298 let direct = lex("\\char `\\%");
1299 assert!(
1300 direct
1301 .iter()
1302 .any(|t| t.kind == SyntaxKind::WORD && t.text == "`\\%")
1303 );
1304 let intervened = lex("\\MakeShortVerb{\\|} \\char |a| `\\%");
1305 assert!(
1306 intervened
1307 .iter()
1308 .any(|t| t.kind == SyntaxKind::VERB && t.text == "|a|")
1309 );
1310 assert!(
1311 !intervened
1312 .iter()
1313 .any(|t| t.kind == SyntaxKind::WORD && t.text == "`\\%")
1314 );
1315 assert_lossless("\\MakeShortVerb{\\|} \\char |a| `\\%");
1316 }
1317
1318 #[test]
1319 fn block_environment_classification() {
1320 let ctx = ParseCtx::default();
1321 assert!(ctx.is_block_environment("figure"));
1322 assert!(ctx.is_block_environment("itemize")); assert!(!ctx.is_block_environment("myenv")); }
1325
1326 #[test]
1327 fn lossless_on_assorted_inputs() {
1328 for input in [
1329 "",
1330 "plain text",
1331 r"\section{Hi}[x]",
1332 "$a^2_b$",
1333 "a%c\n\nb",
1334 "café ∑ \\\\ \\{ \\,",
1335 "tab\tand spaces",
1336 "trailing\\",
1337 r"\verb|$x$|",
1338 "\\begin{verbatim}\n$x$ %not a comment\n\\end{verbatim}",
1339 "\\begin{lstlisting}[language=C]\nint a[3]; % raw\n\\end{lstlisting}",
1340 "\\begin{minted}[frame=single]{python}\nprint(\"$x$\")\n\\end{minted}",
1341 "\\begin{lstlisting}\n[1,2,3]\n\\end{lstlisting}",
1342 r"\makeatletter\a@b\makeatother\a@b",
1343 r"\ExplSyntaxOn\seq_new:N \g_@@_x_tl a_b\ExplSyntaxOff\seq_new:N",
1344 r"$\left(x+y\right)^2 \left.\frac{a}{b}\right|_0$",
1345 ] {
1346 assert_lossless(input);
1347 }
1348 }
1349
1350 #[test]
1351 fn control_word_stops_at_non_letter() {
1352 let toks = lex(r"\alpha2");
1353 assert_eq!(toks[0].kind, SyntaxKind::CONTROL_WORD);
1354 assert_eq!(toks[0].text, "\\alpha");
1355 assert_eq!(toks[1].kind, SyntaxKind::WORD);
1356 assert_eq!(toks[1].text, "2");
1357 }
1358
1359 #[test]
1360 fn double_backslash_is_one_control_symbol() {
1361 let toks = lex(r"\\");
1362 assert_eq!(toks.len(), 1);
1363 assert_eq!(toks[0].kind, SyntaxKind::CONTROL_SYMBOL);
1364 assert_eq!(toks[0].text, r"\\");
1365 }
1366
1367 #[test]
1368 fn comment_stops_before_newline() {
1369 let toks = lex("% hi\nx");
1370 assert_eq!(toks[0].kind, SyntaxKind::COMMENT);
1371 assert_eq!(toks[0].text, "% hi");
1372 assert_eq!(toks[1].kind, SyntaxKind::NEWLINE);
1373 }
1374
1375 #[test]
1376 fn crlf_is_a_single_newline() {
1377 let toks = lex("a\r\nb");
1378 assert_eq!(toks[1].kind, SyntaxKind::NEWLINE);
1379 assert_eq!(toks[1].text, "\r\n");
1380 }
1381
1382 #[test]
1383 fn control_symbol_swallows_the_whole_line_ending() {
1384 for ending in ["\n", "\r", "\r\n"] {
1385 let input = format!("\\{ending}");
1386 let toks = lex(&input);
1387 assert_eq!(toks.len(), 1, "split line ending {ending:?}");
1388 assert_eq!(toks[0].kind, SyntaxKind::CONTROL_SYMBOL);
1389 assert_eq!(toks[0].text, input);
1390 }
1391 }
1392
1393 #[test]
1394 fn verb_inline_is_one_token() {
1395 let toks = lex(r"\verb|$x$|");
1396 assert_eq!(toks.len(), 1);
1397 assert_eq!(toks[0].kind, SyntaxKind::VERB);
1398 assert_eq!(toks[0].text, r"\verb|$x$|");
1399 }
1400
1401 #[test]
1402 fn verb_star_with_plus_delimiter() {
1403 let toks = lex(r"a\verb*+b+c");
1404 assert_eq!(toks[1].kind, SyntaxKind::VERB);
1405 assert_eq!(toks[1].text, r"\verb*+b+");
1406 assert_eq!(toks[2].text, "c");
1407 }
1408
1409 #[test]
1410 fn verb_without_closing_delimiter_is_a_plain_control_word() {
1411 let toks = lex(r"\verb|x");
1412 assert_eq!(toks[0].kind, SyntaxKind::CONTROL_WORD);
1413 assert_eq!(toks[0].text, r"\verb");
1414 }
1415
1416 #[test]
1417 fn left_right_isolate_word_delimiter() {
1418 let toks = lex(r"\left(x+y\right)");
1419 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1420 assert_eq!(
1421 seen,
1422 [
1423 (SyntaxKind::CONTROL_WORD, "\\left"),
1424 (SyntaxKind::WORD, "("),
1425 (SyntaxKind::WORD, "x+y"),
1426 (SyntaxKind::CONTROL_WORD, "\\right"),
1427 (SyntaxKind::WORD, ")"),
1428 ]
1429 );
1430 }
1431
1432 #[test]
1433 fn left_delimiter_carries_across_whitespace() {
1434 let toks = lex(r"\left ( a");
1435 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1436 assert_eq!(
1437 seen,
1438 [
1439 (SyntaxKind::CONTROL_WORD, "\\left"),
1440 (SyntaxKind::WHITESPACE, " "),
1441 (SyntaxKind::WORD, "("),
1442 (SyntaxKind::WHITESPACE, " "),
1443 (SyntaxKind::WORD, "a"),
1444 ]
1445 );
1446 }
1447
1448 #[test]
1449 fn left_non_word_delimiters_are_untouched() {
1450 for input in [r"\left\{", r"\left\langle", r"\left["] {
1451 assert_lossless(input);
1452 }
1453 let toks = lex(r"\left\langle x \right\rangle");
1454 assert!(toks.iter().any(|t| t.text == "\\langle"));
1455 assert!(toks.iter().any(|t| t.text == "\\rangle"));
1456 }
1457
1458 #[test]
1459 fn leftarrow_is_not_left() {
1460 let toks = lex(r"\leftarrow(x)");
1461 assert_eq!(toks[0].text, "\\leftarrow");
1462 assert_eq!(toks[1].text, "(x)");
1463 }
1464
1465 #[test]
1466 fn makeatletter_makes_at_a_letter() {
1467 let toks = lex(r"\makeatletter\foo@bar\makeatother\foo@bar");
1468 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1469 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo@bar")));
1470 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo")));
1471 }
1472
1473 #[test]
1474 fn expl_syntax_makes_underscore_and_colon_letters() {
1475 let toks = lex(r"\ExplSyntaxOn\seq_new:N\ExplSyntaxOff\seq_new:N");
1476 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1477 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq_new:N")));
1478 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq")));
1479 }
1480
1481 #[test]
1482 fn expl_syntax_lexes_internal_double_underscore_name() {
1483 let toks = lex(r"\ExplSyntaxOn\__module_internal:nn");
1484 assert_eq!(toks[1].kind, SyntaxKind::CONTROL_WORD);
1485 assert_eq!(toks[1].text, "\\__module_internal:nn");
1486 }
1487
1488 #[test]
1489 fn provides_expl_package_turns_on_expl_syntax() {
1490 let toks = lex(r"\ProvidesExplPackage{p}{2026/01/01}{1.0}{d}\tl_set:Nn");
1491 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1492 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\tl_set:Nn")));
1493 }
1494
1495 #[test]
1496 fn expl_syntax_composes_with_makeatletter() {
1497 let toks = lex(r"\makeatletter\ExplSyntaxOn\g_@@_frame_title_tl");
1498 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1499 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\g_@@_frame_title_tl")));
1500 }
1501
1502 #[test]
1503 fn expl_syntax_makes_bare_underscore_a_word_not_subscript() {
1504 let toks = lex(r"\ExplSyntaxOn a_b");
1505 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1506 assert!(seen.contains(&(SyntaxKind::WORD, "a_b")));
1507 assert!(!seen.iter().any(|(k, _)| *k == SyntaxKind::UNDERSCORE));
1508 }
1509
1510 fn lex_dtx(input: &str) -> Vec<Token> {
1511 lex_with(
1512 input,
1513 &ParseCtx::default(),
1514 LexConfig {
1515 flavor: LatexFlavor::Document,
1516 dtx: true,
1517 },
1518 )
1519 }
1520
1521 #[test]
1522 fn implicit_expl_module_guard_makes_macrocode_body_expl3() {
1523 let toks = lex_dtx(
1524 "%<@@=mod>\n\
1525 % \\begin{macrocode}\n\
1526 \\seq_new:N\n\
1527 % \\end{macrocode}\n",
1528 );
1529 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1530 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq_new:N")));
1531 }
1532
1533 #[test]
1534 fn no_expl_signal_leaves_macrocode_body_plain() {
1535 let toks = lex_dtx(
1536 "% \\begin{macrocode}\n\
1537 \\seq_new:N\n\
1538 % \\end{macrocode}\n",
1539 );
1540 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1541 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq")));
1542 assert!(!seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq_new:N")));
1543 }
1544
1545 #[test]
1546 fn implicit_expl_provides_expl_flags_every_body_regardless_of_order() {
1547 let toks = lex_dtx(
1548 "% \\begin{macrocode}\n\
1549 \\seq_new:N\n\
1550 % \\end{macrocode}\n\
1551 % \\ProvidesExplPackage{p}{2026/01/01}{1.0}{d}\n\
1552 % \\begin{macrocode}\n\
1553 \\tl_set:Nn\n\
1554 % \\end{macrocode}\n",
1555 );
1556 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1557 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq_new:N")));
1558 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\tl_set:Nn")));
1559 }
1560
1561 #[test]
1562 fn implicit_expl_is_body_only_doc_layer_stays_plain() {
1563 let toks = lex_dtx(
1564 "%<@@=mod>\n\
1565 % a_b\n\
1566 % \\begin{macrocode}\n\
1567 c_d\n\
1568 % \\end{macrocode}\n",
1569 );
1570 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1571 assert!(seen.contains(&(SyntaxKind::WORD, "c_d")));
1572 assert!(seen.iter().any(|(k, _)| *k == SyntaxKind::UNDERSCORE));
1573 }
1574
1575 #[test]
1576 fn implicit_expl_explicit_off_wins_then_next_body_re_enters() {
1577 let toks = lex_dtx(
1578 "%<@@=mod>\n\
1579 % \\begin{macrocode}\n\
1580 \\seq_new:N\n\
1581 \\ExplSyntaxOff\n\
1582 a_b\n\
1583 % \\end{macrocode}\n\
1584 % \\begin{macrocode}\n\
1585 \\tl_set:Nn\n\
1586 % \\end{macrocode}\n",
1587 );
1588 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1589 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq_new:N")));
1590 assert!(seen.iter().any(|(k, _)| *k == SyntaxKind::UNDERSCORE));
1591 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\tl_set:Nn")));
1592 }
1593
1594 #[test]
1595 fn implicit_expl_gated_off_outside_dtx() {
1596 let toks = lex_with(
1597 "%<@@=mod>\n\\seq_new:N",
1598 &ParseCtx::default(),
1599 LexConfig {
1600 flavor: LatexFlavor::Package,
1601 dtx: false,
1602 },
1603 );
1604 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1605 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq")));
1606 assert!(!seen.contains(&(SyntaxKind::CONTROL_WORD, "\\seq_new:N")));
1607 }
1608
1609 #[test]
1610 fn package_flavor_starts_in_letter_mode() {
1611 let toks = lex_with(
1612 r"\foo@bar",
1613 &ParseCtx::default(),
1614 LatexFlavor::Package.into(),
1615 );
1616 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1617 assert_eq!(seen, vec![(SyntaxKind::CONTROL_WORD, "\\foo@bar")]);
1618 }
1619
1620 #[test]
1621 fn package_flavor_respects_trailing_makeatother() {
1622 let toks = lex_with(
1623 r"\foo@bar\makeatother\foo@bar",
1624 &ParseCtx::default(),
1625 LatexFlavor::Package.into(),
1626 );
1627 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1628 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo@bar")));
1629 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo")));
1630 }
1631
1632 #[test]
1633 fn document_flavor_keeps_at_non_letter() {
1634 let toks = lex(r"\foo@bar");
1635 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1636 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo")));
1637 assert!(!seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo@bar")));
1638 }
1639
1640 #[test]
1641 fn dtx_mode_lexes_line_leading_percent_as_a_margin() {
1642 let dtx = LexConfig {
1643 flavor: LatexFlavor::Document,
1644 dtx: true,
1645 };
1646 let toks = lex_with("% \\foo\nbar % tail\n", &ParseCtx::default(), dtx);
1647 let seen: Vec<_> = toks.iter().map(|t| (t.kind, t.text.as_str())).collect();
1648 assert_eq!(seen[0], (SyntaxKind::DOC_MARGIN, "%"));
1649 assert!(seen.contains(&(SyntaxKind::CONTROL_WORD, "\\foo")));
1650 assert!(seen.contains(&(SyntaxKind::COMMENT, "% tail")));
1651 assert_eq!(
1652 seen.iter()
1653 .filter(|(k, _)| *k == SyntaxKind::DOC_MARGIN)
1654 .count(),
1655 1
1656 );
1657 }
1658
1659 #[test]
1660 fn dtx_mode_is_off_by_default_for_margins_and_guards() {
1661 let plain = lex("% \\foo\n");
1662 assert_eq!(plain[0].kind, SyntaxKind::COMMENT);
1663 let plain_guard = lex("%<*driver>\n");
1664 assert_eq!(plain_guard[0].kind, SyntaxKind::COMMENT);
1665 assert_eq!(plain_guard[0].text, "%<*driver>");
1666 }
1667
1668 #[test]
1669 fn dtx_mode_lexes_line_leading_guards() {
1670 let dtx = LexConfig {
1671 flavor: LatexFlavor::Document,
1672 dtx: true,
1673 };
1674 let block = lex_with("%<*driver>\n%</driver>\n", &ParseCtx::default(), dtx);
1675 assert_eq!(block[0].kind, SyntaxKind::GUARD);
1676 assert_eq!(block[0].text, "%<*driver>");
1677 assert!(
1678 block
1679 .iter()
1680 .any(|t| t.kind == SyntaxKind::GUARD && t.text == "%</driver>")
1681 );
1682 let inline = lex_with("%<plain>\\RequirePackage{x}\n", &ParseCtx::default(), dtx);
1683 assert_eq!(inline[0].kind, SyntaxKind::GUARD);
1684 assert_eq!(inline[0].text, "%<plain>");
1685 assert!(
1686 inline
1687 .iter()
1688 .any(|t| t.kind == SyntaxKind::CONTROL_WORD && t.text == "\\RequirePackage")
1689 );
1690 let expr = lex_with("%<*package|driver>\n", &ParseCtx::default(), dtx);
1691 assert_eq!(expr[0].kind, SyntaxKind::GUARD);
1692 assert_eq!(expr[0].text, "%<*package|driver>");
1693 let midline = lex_with("a %<x>\n", &ParseCtx::default(), dtx);
1694 assert!(
1695 midline
1696 .iter()
1697 .any(|t| t.kind == SyntaxKind::COMMENT && t.text == "%<x>")
1698 );
1699 assert!(!midline.iter().any(|t| t.kind == SyntaxKind::GUARD));
1700 let malformed = lex_with("%<unterminated\n", &ParseCtx::default(), dtx);
1701 assert_eq!(malformed[0].kind, SyntaxKind::COMMENT);
1702 assert_eq!(malformed[0].text, "%<unterminated");
1703 }
1704
1705 #[test]
1706 fn verbatim_environment_body_is_one_raw_token() {
1707 let toks = lex("\\begin{verbatim}\n$not$ %literal\n\\end{verbatim}");
1708 assert_eq!(toks[0].text, "\\begin");
1709 assert_eq!(toks[2].text, "verbatim");
1710 assert!(
1711 toks.iter()
1712 .any(|t| t.kind == SyntaxKind::VERBATIM_BODY && t.text.contains("$not$ %literal"))
1713 );
1714 assert!(!toks.iter().any(|t| t.kind == SyntaxKind::DOLLAR));
1715 assert!(!toks.iter().any(|t| t.kind == SyntaxKind::COMMENT));
1716 }
1717
1718 #[test]
1719 fn argument_taking_verbatim_separates_args_from_body() {
1720 let toks = lex("\\begin{minted}[frame=single]{python}\nprint(\"$x$\")\n\\end{minted}");
1721 let kinds: Vec<_> = toks.iter().map(|t| t.kind).collect();
1722 assert!(kinds.contains(&SyntaxKind::L_BRACKET));
1723 assert!(kinds.contains(&SyntaxKind::R_BRACKET));
1724 assert!(kinds.contains(&SyntaxKind::L_BRACE));
1725 assert!(
1726 toks.iter()
1727 .any(|t| t.kind == SyntaxKind::VERBATIM_BODY && t.text.contains("print(\"$x$\")"))
1728 );
1729 assert!(!toks.iter().any(|t| t.kind == SyntaxKind::DOLLAR));
1730 }
1731
1732 #[test]
1733 fn verbatim_body_starting_with_bracket_is_not_an_argument() {
1734 let toks = lex("\\begin{lstlisting}\n[1,2,3]\n\\end{lstlisting}");
1735 assert!(
1736 !toks
1737 .iter()
1738 .take_while(|t| t.kind != SyntaxKind::VERBATIM_BODY)
1739 .any(|t| t.kind == SyntaxKind::L_BRACKET),
1740 "the bracket on the body's first line must not be lexed as an argument"
1741 );
1742 assert!(
1743 toks.iter()
1744 .any(|t| t.kind == SyntaxKind::VERBATIM_BODY && t.text.contains("[1,2,3]"))
1745 );
1746 }
1747
1748 #[test]
1749 fn make_short_verb_toggles_pipe_capture() {
1750 let toks = lex("|a| \\MakeShortVerb{\\|} |$| \\DeleteShortVerb{\\|} |b|");
1751 let verbs: Vec<_> = toks
1752 .iter()
1753 .filter(|t| t.kind == SyntaxKind::VERB)
1754 .map(|t| t.text.as_str())
1755 .collect();
1756 assert_eq!(verbs, ["|$|"]);
1757 assert_lossless("|a| \\MakeShortVerb{\\|} |$| \\DeleteShortVerb{\\|} |b|");
1758 }
1759
1760 #[test]
1761 fn documentclass_ltxguide_enables_the_pipe_short_verb() {
1762 for preamble in [
1763 "\\documentclass{ltxguide}",
1764 "\\documentclass[a4paper]{ltxdoc}",
1765 "\\documentclass{ltxguide}[1994/11/20]",
1766 "\\documentclass{l3doc}",
1767 "\\documentclass[leqno,titlepage]{amsldoc}[1999/12/13]",
1768 ] {
1769 let input = format!("{preamble}\n|}}| done");
1770 let toks = lex(&input);
1771 assert!(
1772 toks.iter()
1773 .any(|t| t.kind == SyntaxKind::VERB && t.text == "|}|"),
1774 "no VERB captured after {preamble}"
1775 );
1776 }
1777 let toks = lex("\\documentclass{article}\n|x| done");
1778 assert!(!toks.iter().any(|t| t.kind == SyntaxKind::VERB));
1779 }
1780
1781 #[test]
1782 fn short_verb_never_captures_a_left_right_delimiter() {
1783 let toks = lex("\\MakeShortVerb{\\|} $\\left|x\\right|$");
1784 assert!(!toks.iter().any(|t| t.kind == SyntaxKind::VERB));
1785 assert_lossless("\\MakeShortVerb{\\|} $\\left|x\\right|$");
1786 }
1787
1788 #[test]
1789 fn unclosed_short_verb_char_stands_alone() {
1790 let toks = lex("\\MakeShortVerb{\\|} a|b\nc");
1791 assert!(!toks.iter().any(|t| t.kind == SyntaxKind::VERB));
1792 assert!(
1793 toks.iter()
1794 .any(|t| t.kind == SyntaxKind::WORD && t.text == "|")
1795 );
1796 assert_lossless("\\MakeShortVerb{\\|} a|b\nc");
1797 }
1798
1799 #[test]
1800 fn raw_capture_content_does_not_change_later_lexing() {
1801 const ENV_BODIES: &[&str] = &[
1802 "",
1803 "plain text",
1804 "\\makeatletter",
1805 "\\ExplSyntaxOn",
1806 "\\MakeShortVerb{\\|}",
1807 "{{{",
1808 "}}}",
1809 "% not a comment",
1810 "$ & # ^ _ ~",
1811 "\\end{verbatimx}",
1812 "\\begin{verbatim}",
1813 "\\char`{",
1814 "\\left(",
1815 ];
1816 const INLINE_BODIES: &[&str] = &[
1817 "",
1818 "x",
1819 "\\makeatletter",
1820 "\\ExplSyntaxOn",
1821 "{}",
1822 "$ & # ^ _ ~",
1823 "% not a comment",
1824 "\\char`",
1825 ];
1826 const SUFFIX: &str = "after \\my@cmd \\l_tmpa_tl |bar| \\char`{ \\left( x\n";
1827
1828 for (prefix, open, close, bodies) in [
1829 (
1830 "before x\n",
1831 "\\begin{verbatim}\n",
1832 "\n\\end{verbatim}\n",
1833 ENV_BODIES,
1834 ),
1835 (
1836 "before x\n",
1837 "\\begin{lstlisting}[a=b]\n",
1838 "\n\\end{lstlisting}\n",
1839 ENV_BODIES,
1840 ),
1841 ("before x ", "\\verb+", "+ ", INLINE_BODIES),
1842 ("before x ", "\\url{", "} ", INLINE_BODIES),
1843 ("before x ", "\\href{", "}{visible} ", INLINE_BODIES),
1844 ("before x ", "\\lstinline+", "+ ", INLINE_BODIES),
1845 ] {
1846 let mut expected: Option<Vec<(SyntaxKind, String)>> = None;
1847 for body in bodies {
1848 let region = format!("{open}{body}{close}");
1849 let doc = format!("{prefix}{region}{SUFFIX}");
1850 assert_lossless(&doc);
1851
1852 let toks = lex(&doc);
1853 assert!(
1854 toks.iter()
1855 .any(|t| matches!(t.kind, SyntaxKind::VERB | SyntaxKind::VERBATIM_BODY))
1856 || body.is_empty(),
1857 "no raw capture formed, so this case proves nothing\n \
1858 region: {region:?}",
1859 );
1860
1861 let from = prefix.len() + region.len();
1862 let mut off = 0usize;
1863 let got: Vec<(SyntaxKind, String)> = toks
1864 .into_iter()
1865 .filter(|t| {
1866 let start = off;
1867 off += t.text.len();
1868 start >= from
1869 })
1870 .map(|t| (t.kind, t.text.to_string()))
1871 .collect();
1872
1873 match &expected {
1874 None => expected = Some(got),
1875 Some(want) => assert_eq!(
1876 &got, want,
1877 "a raw body changed how the text after it lexes\n \
1878 region: {region:?}",
1879 ),
1880 }
1881 }
1882 }
1883 }
1884
1885 #[test]
1886 fn a_body_that_breaks_its_capture_changes_later_lexing() {
1887 let captured = lex("\\url{x} \\char`{");
1888 assert!(captured.iter().any(|t| t.kind == SyntaxKind::VERB));
1889 assert!(
1890 captured
1891 .iter()
1892 .any(|t| t.kind == SyntaxKind::WORD && t.text == "`{")
1893 );
1894
1895 let broken = lex("\\url{{} \\char`{");
1896 assert!(!broken.iter().any(|t| t.kind == SyntaxKind::VERB));
1897 assert!(
1898 broken
1899 .iter()
1900 .any(|t| t.kind == SyntaxKind::WORD && t.text == "`")
1901 );
1902 }
1903}