jsslint_core/tex/parser.rs
1//! Tolerant LaTeX tokenizer — a from-scratch parser producing the same
2//! six node kinds as `pylatexenc.latexwalker`, tuned to match its
3//! observed behavior rather than general TeX semantics. Ground truth
4//! was established empirically against the installed `pylatexenc`
5//! (see git history / plan notes for the probe sessions), not by
6//! reading its source line-by-line — where the two might diverge, the
7//! `tests/parser_parity.rs` fixture-diff harness is the actual
8//! arbiter, per the plan's Phase 1 scope ("no rules yet — just prove
9//! the substrate matches").
10//!
11//! Key empirically-verified rules this parser encodes:
12//!
13//! 1. **Control-word whitespace eating.** A control WORD (`\` + one or
14//! more `[A-Za-z@]`) swallows trailing spaces/tabs and *at most one*
15//! newline (plus further spaces/tabs after it) into its own span —
16//! UNLESS eating that newline would immediately cross a blank line
17//! (two adjacent `\n`s), which is left untouched as a paragraph
18//! boundary. A control SYMBOL (`\` + exactly one other character)
19//! does not eat anything.
20//! 2. **Unknown macros take zero arguments.** A macro/environment name
21//! absent from `specs::macro_argspec` (compiled from pylatexenc's
22//! own default database — see `tex/specs.rs`) ends immediately after
23//! its (whitespace-eaten) name. Whatever follows — commonly a
24//! `{...}` group — is simply the next sibling node in the parent's
25//! list, not an argument. This is *why* JSS's own macros (`\pkg`,
26//! `\proglang`, `\code`, ...) need the sibling-group fallback that
27//! `tex::prose` ports from `_helpers.py::_macro_args_text`.
28//! 3. **Known macros/environments** parse arguments per an argspec
29//! string (`{` mandatory group, `[` optional bracket group, `*`
30//! optional literal star), each preceded by a TeX-whitespace skip
31//! (rule 1's skipping logic, reused) — except a `[` slot that finds
32//! no `[` does NOT consume the whitespace it peeked past.
33//! 4. **`\[`/`\]` and `\(`/`\)`** are recognized as literal display/
34//! inline math delimiters at the tokenizer level, not as generic
35//! control symbols.
36//!
37//! Known, deliberate simplifications:
38//!
39//! - An *unbraced* mandatory argument (`\section\relax text`,
40//! vanishingly rare in real manuscripts) reads only a single bare
41//! token (one char, or one control-sequence *name* with no further
42//! recursive argument parsing), rather than pylatexenc's fully
43//! general recursive expression read.
44//! - Inside a *deliberately unclosed* `{` group (malformed input),
45//! this tokenizer keeps tokenizing normally, while real pylatexenc's
46//! tolerant-mode recovery silently drops a stray `\end{...}` token
47//! encountered there — see `tests/parser_parity.rs`'s
48//! `KNOWN_DIVERGENCES` for the one fixture this affects.
49//!
50//! Revisit either only if the fixture-diff harness surfaces a real
51//! corpus file that needs it.
52
53use super::node::*;
54use super::specs;
55
56/// What ends the current `parse_nodelist` call. The matched stop text
57/// itself is consumed (its end position is returned) but never appears
58/// in the returned node list.
59#[derive(Debug, Clone)]
60enum Stop {
61 /// Top-level document parse — nothing but end-of-input stops it.
62 EndOfInput,
63 CloseBrace,
64 CloseBracket,
65 /// A single, undoubled `$`.
66 DollarMath,
67 /// `$$`.
68 DisplayDollarMath,
69 /// Literal `\]`.
70 MathBracket,
71 /// Literal `\)`.
72 MathParen,
73 /// Literal `\end{<name>}`, exact name match.
74 EndEnvironment(String),
75}
76
77pub struct Parser<'a> {
78 chars: &'a [char],
79}
80
81/// Parse `source` into a top-level node list. Never fails — unclosed
82/// groups/environments/math simply extend to end-of-input, matching
83/// this crate's tolerant-only philosophy (the Python side has a
84/// separate strict-then-tolerant retry; this port only implements the
85/// tolerant behavior, which is what most real documents already
86/// resolve to after `core/parser.py`'s pre-neutralization).
87pub fn parse(source: &str) -> (Vec<Node>, Vec<char>) {
88 let chars: Vec<char> = source.chars().collect();
89 let (nodes, _end) = {
90 let parser = Parser { chars: &chars };
91 parser.parse_nodelist(0, &Stop::EndOfInput)
92 };
93 (nodes, chars)
94}
95
96impl<'a> Parser<'a> {
97 fn len(&self) -> usize {
98 self.chars.len()
99 }
100
101 fn parse_nodelist(&self, mut pos: usize, stop: &Stop) -> (Vec<Node>, usize) {
102 let mut nodes: Vec<Node> = Vec::new();
103 let mut chars_start: Option<usize> = None;
104 let n = self.len();
105
106 while pos < n {
107 if let Some(stop_end) = self.matches_stop(pos, stop) {
108 self.flush_chars(&mut nodes, &mut chars_start, pos);
109 return (nodes, stop_end);
110 }
111 if let Some(matched) = self.match_specials(pos) {
112 self.flush_chars(&mut nodes, &mut chars_start, pos);
113 let len = matched.chars().count();
114 nodes.push(Node::Specials(SpecialsNode {
115 span: Span { pos, len },
116 chars: matched.to_string(),
117 }));
118 pos += len;
119 continue;
120 }
121 match self.chars[pos] {
122 '%' => {
123 self.flush_chars(&mut nodes, &mut chars_start, pos);
124 let (node, next) = self.parse_comment(pos);
125 nodes.push(Node::Comment(node));
126 pos = next;
127 }
128 '\\' => {
129 self.flush_chars(&mut nodes, &mut chars_start, pos);
130 let (node, next) = self.parse_backslash(pos);
131 nodes.push(node);
132 pos = next;
133 }
134 '{' => {
135 self.flush_chars(&mut nodes, &mut chars_start, pos);
136 let (node, next) = self.parse_group(pos, GroupDelims::Brace);
137 nodes.push(Node::Group(node));
138 pos = next;
139 }
140 '$' => {
141 self.flush_chars(&mut nodes, &mut chars_start, pos);
142 let (node, next) = self.parse_dollar_math(pos);
143 nodes.push(Node::Math(node));
144 pos = next;
145 }
146 _ => {
147 if chars_start.is_none() {
148 chars_start = Some(pos);
149 }
150 pos += 1;
151 }
152 }
153 }
154 self.flush_chars(&mut nodes, &mut chars_start, pos);
155 (nodes, pos)
156 }
157
158 fn flush_chars(&self, nodes: &mut Vec<Node>, chars_start: &mut Option<usize>, end: usize) {
159 if let Some(start) = chars_start.take() {
160 if end > start {
161 nodes.push(Node::Chars(CharsNode {
162 span: Span {
163 pos: start,
164 len: end - start,
165 },
166 chars: self.chars[start..end].iter().collect(),
167 }));
168 }
169 }
170 }
171
172 /// `None` if `stop` doesn't match at `pos`; otherwise the position
173 /// right after the consumed stop text.
174 fn matches_stop(&self, pos: usize, stop: &Stop) -> Option<usize> {
175 match stop {
176 Stop::EndOfInput => None,
177 Stop::CloseBrace => (self.chars[pos] == '}').then_some(pos + 1),
178 Stop::CloseBracket => (self.chars[pos] == ']').then_some(pos + 1),
179 Stop::DollarMath => {
180 (self.chars[pos] == '$' && !self.starts_with(pos, "$$")).then_some(pos + 1)
181 }
182 Stop::DisplayDollarMath => self.starts_with(pos, "$$").then_some(pos + 2),
183 Stop::MathBracket => self.starts_with(pos, r"\]").then_some(pos + 2),
184 Stop::MathParen => self.starts_with(pos, r"\)").then_some(pos + 2),
185 Stop::EndEnvironment(name) => {
186 let marker = format!(r"\end{{{name}}}");
187 self.starts_with(pos, &marker)
188 .then_some(pos + marker.chars().count())
189 }
190 }
191 }
192
193 fn starts_with(&self, pos: usize, needle: &str) -> bool {
194 let needle_chars: Vec<char> = needle.chars().collect();
195 if pos + needle_chars.len() > self.len() {
196 return false;
197 }
198 self.chars[pos..pos + needle_chars.len()] == needle_chars[..]
199 }
200
201 /// The longest `specs::specials()` entry matching literally at
202 /// `pos`, if any (the table is already longest-first).
203 fn match_specials(&self, pos: usize) -> Option<&'static str> {
204 specs::specials()
205 .iter()
206 .find(|&&s| self.starts_with(pos, s))
207 .copied()
208 }
209
210 /// TeX whitespace skip (control-word / macro post-space eating).
211 /// Mirrors pylatexenc's tokenizer loop exactly: consume ANY
212 /// Unicode-whitespace char (not just space/tab/`\n` — `\r`, form
213 /// feed, etc. all count, like Python's `str.isspace()`), UNLESS
214 /// doing so would make the just-consumed run end with a LITERAL
215 /// `"\n\n"` (two LF in a row — a paragraph boundary), in which case
216 /// back off those 2 chars and stop. This is a literal `"\n\n"`
217 /// check, not "any two newline-class chars in a row": a `\r\n\r\n`
218 /// blank line (CRLF) does NOT trigger it and is consumed in full —
219 /// see module docs rule 1.
220 fn skip_tex_whitespace(&self, pos: usize) -> usize {
221 let n = self.len();
222 let mut i = pos;
223 while i < n && self.chars[i].is_whitespace() {
224 i += 1;
225 if i - pos >= 2 && self.chars[i - 2] == '\n' && self.chars[i - 1] == '\n' {
226 i -= 2;
227 break;
228 }
229 }
230 i
231 }
232
233 fn parse_comment(&self, pos: usize) -> (CommentNode, usize) {
234 debug_assert_eq!(self.chars[pos], '%');
235 let n = self.len();
236 let mut i = pos + 1;
237 // `\r` terminates a comment exactly like `\n` — mirrors
238 // pylatexenc's tokenizer regex `(\n|\r|\n\r)(?P<extraspace>\s*)`,
239 // which matches whichever of `\n`/`\r` occurs first (a bare `\r`
240 // stops the comment scan on its own, `\r\n` is NOT treated as
241 // one atomic terminator).
242 while i < n && self.chars[i] != '\n' && self.chars[i] != '\r' {
243 i += 1;
244 }
245 let text_end = i;
246 // Include the terminating newline in the span (not the text)
247 // UNLESS doing so would cross into a blank line — same rule as
248 // control-word whitespace-eating (`skip_tex_whitespace`); empirically
249 // verified against pylatexenc (module docs rule 1 applies here too).
250 // `\r` counts as a newline for THIS check too: since a bare `\r`
251 // is itself "the terminator" the regex matches, a `\r\n` pair
252 // means the very next char after that terminator is ALSO
253 // newline-class, so pylatexenc's paragraph-break branch fires —
254 // net effect: a `\r\n` line ending is never consumed into a
255 // comment's span at all (differs from a plain `\n`, which IS
256 // consumed when not followed by a second newline).
257 let is_newline_class = |c: char| c == '\n' || c == '\r';
258 let span_end = if i < n && !(i + 1 < n && is_newline_class(self.chars[i + 1])) {
259 i + 1
260 } else {
261 i
262 };
263 (
264 CommentNode {
265 span: Span {
266 pos,
267 len: span_end - pos,
268 },
269 comment: self.chars[pos + 1..text_end].iter().collect(),
270 },
271 span_end,
272 )
273 }
274
275 fn parse_group(&self, pos: usize, delims: GroupDelims) -> (GroupNode, usize) {
276 debug_assert!(self.chars[pos] == '{' || self.chars[pos] == '[');
277 let stop = match delims {
278 GroupDelims::Brace => Stop::CloseBrace,
279 GroupDelims::Bracket => Stop::CloseBracket,
280 };
281 let (nodelist, end) = self.parse_nodelist(pos + 1, &stop);
282 (
283 GroupNode {
284 span: Span {
285 pos,
286 len: end - pos,
287 },
288 delims,
289 nodelist,
290 },
291 end,
292 )
293 }
294
295 fn parse_dollar_math(&self, pos: usize) -> (MathNode, usize) {
296 debug_assert_eq!(self.chars[pos], '$');
297 if self.starts_with(pos, "$$") {
298 let (nodelist, end) = self.parse_nodelist(pos + 2, &Stop::DisplayDollarMath);
299 (
300 MathNode {
301 span: Span {
302 pos,
303 len: end - pos,
304 },
305 kind: MathKind::Display,
306 nodelist,
307 },
308 end,
309 )
310 } else {
311 let (nodelist, end) = self.parse_nodelist(pos + 1, &Stop::DollarMath);
312 (
313 MathNode {
314 span: Span {
315 pos,
316 len: end - pos,
317 },
318 kind: MathKind::Inline,
319 nodelist,
320 },
321 end,
322 )
323 }
324 }
325
326 /// Dispatch on what follows a `\`: `\[`/`\(` math openers,
327 /// `\begin`/`\end`, a verbatim macro, a known macro (argspec
328 /// lookup), or an unknown zero-arg macro.
329 fn parse_backslash(&self, pos: usize) -> (Node, usize) {
330 debug_assert_eq!(self.chars[pos], '\\');
331 let n = self.len();
332 if pos + 1 >= n {
333 // Trailing lone backslash at EOF — degrade to an empty-name macro.
334 return (
335 Node::Macro(MacroNode {
336 span: Span { pos, len: 1 },
337 macroname: String::new(),
338 args: Vec::new(),
339 }),
340 pos + 1,
341 );
342 }
343
344 if self.starts_with(pos, r"\[") {
345 let (nodelist, end) = self.parse_nodelist(pos + 2, &Stop::MathBracket);
346 return (
347 Node::Math(MathNode {
348 span: Span {
349 pos,
350 len: end - pos,
351 },
352 kind: MathKind::Display,
353 nodelist,
354 }),
355 end,
356 );
357 }
358 if self.starts_with(pos, r"\(") {
359 let (nodelist, end) = self.parse_nodelist(pos + 2, &Stop::MathParen);
360 return (
361 Node::Math(MathNode {
362 span: Span {
363 pos,
364 len: end - pos,
365 },
366 kind: MathKind::Inline,
367 nodelist,
368 }),
369 end,
370 );
371 }
372
373 let next = self.chars[pos + 1];
374 let (name, name_end, is_word) = if next.is_ascii_alphabetic() || next == '@' {
375 let mut i = pos + 1;
376 while i < n && (self.chars[i].is_ascii_alphabetic() || self.chars[i] == '@') {
377 i += 1;
378 }
379 (self.chars[pos + 1..i].iter().collect::<String>(), i, true)
380 } else {
381 (next.to_string(), pos + 2, false)
382 };
383
384 let after_ws = if is_word {
385 self.skip_tex_whitespace(name_end)
386 } else {
387 name_end
388 };
389
390 if is_word && name == "begin" {
391 return self.parse_environment(pos, after_ws);
392 }
393 // A stray `\end` not consumed as a body's Stop::EndEnvironment
394 // (e.g. mismatched/unbalanced input) — tolerant fallback: treat
395 // as an ordinary (unknown) zero-arg macro.
396 if is_word && specs::is_verbatim_macro(&name) {
397 return self.parse_verbatim_macro(pos, name, after_ws);
398 }
399 if let Some(argspec) = specs::macro_argspec(&name) {
400 let (args, end) = self.parse_args(argspec, after_ws);
401 return (
402 Node::Macro(MacroNode {
403 span: Span {
404 pos,
405 len: end - pos,
406 },
407 macroname: name,
408 args,
409 }),
410 end,
411 );
412 }
413 // Unknown macro: zero args. Trailing whitespace already eaten
414 // into `after_ws` for a control word; a control symbol eats
415 // nothing (rule 1).
416 (
417 Node::Macro(MacroNode {
418 span: Span {
419 pos,
420 len: after_ws - pos,
421 },
422 macroname: name,
423 args: Vec::new(),
424 }),
425 after_ws,
426 )
427 }
428
429 /// `\verb<delim>...<delim>` (and similarly-shaped verbatim macros):
430 /// the character immediately after (whitespace-eaten) name-end is
431 /// the delimiter; content runs verbatim until it repeats.
432 fn parse_verbatim_macro(&self, pos: usize, name: String, after_ws: usize) -> (Node, usize) {
433 let n = self.len();
434 if after_ws >= n {
435 return (
436 Node::Macro(MacroNode {
437 span: Span {
438 pos,
439 len: after_ws - pos,
440 },
441 macroname: name,
442 args: Vec::new(),
443 }),
444 after_ws,
445 );
446 }
447 let delim = self.chars[after_ws];
448 let content_start = after_ws + 1;
449 let mut i = content_start;
450 while i < n && self.chars[i] != delim {
451 i += 1;
452 }
453 let content_end = i;
454 let span_end = if i < n { i + 1 } else { i }; // include closing delimiter if found
455 let content: String = self.chars[content_start..content_end].iter().collect();
456 let arg = Node::Chars(CharsNode {
457 span: Span {
458 pos: content_start,
459 len: content_end - content_start,
460 },
461 chars: content,
462 });
463 (
464 Node::Macro(MacroNode {
465 span: Span {
466 pos,
467 len: span_end - pos,
468 },
469 macroname: name,
470 args: vec![Some(arg)],
471 }),
472 span_end,
473 )
474 }
475
476 fn parse_environment(&self, macro_start: usize, mut pos: usize) -> (Node, usize) {
477 let n = self.len();
478 // Expect `{name}` immediately (after whitespace, already skipped
479 // by the caller via `after_ws`).
480 if pos >= n || self.chars[pos] != '{' {
481 // Malformed `\begin` with no name — tolerant fallback: an
482 // unknown zero-arg macro named "begin".
483 return (
484 Node::Macro(MacroNode {
485 span: Span {
486 pos: macro_start,
487 len: pos - macro_start,
488 },
489 macroname: "begin".to_string(),
490 args: Vec::new(),
491 }),
492 pos,
493 );
494 }
495 let (name, name_group_end) = self.read_brace_delimited_name(pos);
496 pos = name_group_end;
497
498 let args = if let Some(argspec) = specs::environment_argspec(&name) {
499 let (a, next) = self.parse_args(argspec, pos);
500 pos = next;
501 a
502 } else {
503 Vec::new()
504 };
505
506 if specs::is_verbatim_environment(&name) {
507 return self.parse_verbatim_environment(macro_start, name, pos);
508 }
509
510 let (nodelist, end) = self.parse_nodelist(pos, &Stop::EndEnvironment(name.clone()));
511 (
512 Node::Environment(EnvironmentNode {
513 span: Span {
514 pos: macro_start,
515 len: end - macro_start,
516 },
517 environmentname: name,
518 args,
519 nodelist,
520 }),
521 end,
522 )
523 }
524
525 /// The `verbatim`/`verbatim*` environment: body is read as raw
526 /// text up to (not including further parsing of) the literal
527 /// `\end{name}` marker.
528 fn parse_verbatim_environment(
529 &self,
530 macro_start: usize,
531 name: String,
532 pos: usize,
533 ) -> (Node, usize) {
534 let marker: Vec<char> = format!(r"\end{{{name}}}").chars().collect();
535 let n = self.len();
536 let mut i = pos;
537 while i < n {
538 if i + marker.len() <= n && self.chars[i..i + marker.len()] == marker[..] {
539 break;
540 }
541 i += 1;
542 }
543 let body_end = i;
544 let span_end = if i < n { i + marker.len() } else { i };
545 let body_text: String = self.chars[pos..body_end].iter().collect();
546 // Mirrors pylatexenc's `VerbatimArgsParser`: the raw body lives
547 // in the environment's ARGUMENT list (`nodeargd.argnlist`), not
548 // its `nodelist` — `nodelist` is empty. This matters beyond
549 // cosmetics: `prose::walk`/`_walk_with_context` only recurse
550 // into `nodelist` for an `Environment` node, so a body stored
551 // there would be visible to prose-context walking, while one
552 // stored in `args` (matching pylatexenc) is invisible to it —
553 // and WIDTH-001/CODE-* read `env.nodelist` too, so this also
554 // reproduces Python's (accidental, but real) gap where content
555 // inside a literal `verbatim` env is never width/style-checked.
556 let verbatim_arg = vec![Some(Node::Chars(CharsNode {
557 span: Span {
558 pos,
559 len: body_end - pos,
560 },
561 chars: body_text,
562 }))];
563 (
564 Node::Environment(EnvironmentNode {
565 span: Span {
566 pos: macro_start,
567 len: span_end - macro_start,
568 },
569 environmentname: name,
570 args: verbatim_arg,
571 nodelist: Vec::new(),
572 }),
573 span_end,
574 )
575 }
576
577 /// Reads `{name}` starting at `pos` (which must be `{`), where
578 /// `name` is a raw identifier (no nested macro parsing — matches
579 /// environment names never legitimately containing them).
580 fn read_brace_delimited_name(&self, pos: usize) -> (String, usize) {
581 debug_assert_eq!(self.chars[pos], '{');
582 let n = self.len();
583 let mut i = pos + 1;
584 while i < n && self.chars[i] != '}' {
585 i += 1;
586 }
587 let name: String = self.chars[pos + 1..i].iter().collect();
588 let end = if i < n { i + 1 } else { i };
589 (name, end)
590 }
591
592 /// Parses `argspec` (chars `{`/`[`/`*`) starting at `pos`. See
593 /// module docs rule 3 for the whitespace-consumption asymmetry
594 /// between mandatory and optional slots.
595 fn parse_args(&self, argspec: &str, mut pos: usize) -> (Args, usize) {
596 let mut args: Args = Vec::with_capacity(argspec.chars().count());
597 for slot in argspec.chars() {
598 match slot {
599 '{' => {
600 pos = self.skip_tex_whitespace(pos);
601 if pos < self.len() && self.chars[pos] == '{' {
602 let (g, next) = self.parse_group(pos, GroupDelims::Brace);
603 pos = next;
604 args.push(Some(Node::Group(g)));
605 } else if pos < self.len() {
606 let (node, next) = self.parse_single_token(pos);
607 pos = next;
608 args.push(Some(node));
609 } else {
610 args.push(None);
611 }
612 }
613 '[' => {
614 let peek = self.skip_tex_whitespace(pos);
615 if peek < self.len() && self.chars[peek] == '[' {
616 let (g, next) = self.parse_group(peek, GroupDelims::Bracket);
617 pos = next;
618 args.push(Some(Node::Group(g)));
619 } else {
620 args.push(None); // pos NOT advanced — matches pylatexenc
621 }
622 }
623 '*' => {
624 if pos < self.len() && self.chars[pos] == '*' {
625 args.push(Some(Node::Chars(CharsNode {
626 span: Span { pos, len: 1 },
627 chars: "*".to_string(),
628 })));
629 pos += 1;
630 } else {
631 args.push(None);
632 }
633 }
634 _ => {}
635 }
636 }
637 (args, pos)
638 }
639
640 /// Simplified single-token read for an unbraced mandatory argument
641 /// — see module docs' "known simplification" note.
642 fn parse_single_token(&self, pos: usize) -> (Node, usize) {
643 if self.chars[pos] == '}' {
644 // A lone closing brace can never BE an argument — it's
645 // whatever group we're already inside trying to close.
646 // Mirrors pylatexenc's `get_latex_expression`'s
647 // `tok.tok == 'brace_close'` branch: return an empty node
648 // at this position WITHOUT consuming the brace, so the
649 // enclosing group's own close-brace scan sees it next.
650 // Without this, a macro used bare (no explicit `{...}`) as
651 // the last thing before a group closes — e.g.
652 // `\newcommand{\mbf}{ \mathbf }` — swallows the `}` as
653 // `\mathbf`'s "argument", and the enclosing group never
654 // finds its own terminator, running away to end of input.
655 return (
656 Node::Chars(CharsNode {
657 span: Span { pos, len: 0 },
658 chars: String::new(),
659 }),
660 pos,
661 );
662 }
663 if self.chars[pos] == '\\' {
664 let n = self.len();
665 if pos + 1 >= n {
666 return (
667 Node::Macro(MacroNode {
668 span: Span { pos, len: 1 },
669 macroname: String::new(),
670 args: Vec::new(),
671 }),
672 pos + 1,
673 );
674 }
675 let next = self.chars[pos + 1];
676 if next.is_ascii_alphabetic() || next == '@' {
677 let mut i = pos + 1;
678 while i < n && (self.chars[i].is_ascii_alphabetic() || self.chars[i] == '@') {
679 i += 1;
680 }
681 let end = self.skip_tex_whitespace(i);
682 return (
683 Node::Macro(MacroNode {
684 span: Span {
685 pos,
686 len: end - pos,
687 },
688 macroname: self.chars[pos + 1..i].iter().collect(),
689 args: Vec::new(),
690 }),
691 end,
692 );
693 }
694 return (
695 Node::Macro(MacroNode {
696 span: Span { pos, len: 2 },
697 macroname: next.to_string(),
698 args: Vec::new(),
699 }),
700 pos + 2,
701 );
702 }
703 (
704 Node::Chars(CharsNode {
705 span: Span { pos, len: 1 },
706 chars: self.chars[pos].to_string(),
707 }),
708 pos + 1,
709 )
710 }
711}