ansi/parse.rs
1//! Incremental VT/ANSI tokenizer.
2//!
3//! Two APIs over one state machine, so they can never disagree:
4//!
5//! * [`Parser::feed_with`] is the engine. It calls a closure with an
6//! [`Event`] that **borrows** — text runs point into the caller's buffer,
7//! sequence payloads into the parser's own reused scratch — so tokenizing a
8//! stream allocates nothing after the first few sequences.
9//! * [`Parser::feed`] is the owned convenience API: the same events collected
10//! into a `Vec<Token>`, with text decoded as UTF-8 (invalid bytes become
11//! U+FFFD). It costs a `String`/`Vec` per token, which is why the engine
12//! exists.
13//!
14//! Both accept a byte stream in chunks split at *any* boundary (mid-escape
15//! sequence, mid-UTF-8 character). The state machine follows the classic
16//! VT500-series design: C0 controls are *executed* (emitted as
17//! [`Event::Control`]) even when they arrive inside an escape sequence,
18//! `CAN`/`SUB` abort a sequence in progress, and a stray `ESC` restarts one.
19//! Malformed sequences are dropped, never emitted as text.
20//!
21//! # Choosing between them
22//!
23//! ```
24//! # use ansi::{Event, Parser};
25//! let mut p = Parser::new();
26//! let mut printable = 0usize;
27//! p.feed_with(b"hi\x1b[1;31mred", |event| {
28//! if let Event::Text(bytes) = event {
29//! printable += bytes.len();
30//! }
31//! });
32//! assert_eq!(printable, 5); // "hi" + "red", no allocation
33//! ```
34
35/// One parsed unit of terminal output, owned.
36///
37/// [`Event`] is the same information borrowed; this is what [`Parser::feed`]
38/// builds from it.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Token {
41 /// A run of printable text (valid UTF-8; invalid input bytes become
42 /// U+FFFD). Runs are flushed at each control byte and at the end of each
43 /// `feed` call, so adjacent `Text` tokens are equivalent to one.
44 Text(String),
45 /// A C0 control byte executed on its own (BEL, BS, TAB, LF, CR, ...) or
46 /// DEL. `ESC` never appears here; it introduces a sequence.
47 Control(u8),
48 /// `ESC [ ...`: a CSI sequence with an optional private marker (`?`, `>`, `<`,
49 /// `=`), numeric parameters, intermediate bytes, and the final byte.
50 /// Missing parameters parse as 0; colon sub-parameters are truncated.
51 Csi {
52 private: Option<char>,
53 params: Vec<u16>,
54 intermediates: Vec<u8>,
55 final_byte: char,
56 },
57 /// A non-CSI escape: `ESC` + optional intermediates + final byte
58 /// (e.g. `ESC 7`, `ESC ( B`).
59 Esc {
60 intermediates: Vec<u8>,
61 final_byte: u8,
62 },
63 /// `ESC ] ...`: an OSC string, terminated by BEL or ST (`ESC \`).
64 /// Payload bytes are decoded as UTF-8 (lossily).
65 Osc(String),
66 /// A DCS/SOS/PM/APC string (`intro` is `P`, `X`, `^`, or `_`), swallowed
67 /// whole and surfaced raw rather than interpreted.
68 Other { intro: u8, data: Vec<u8> },
69}
70
71/// One parsed unit, borrowed: nothing here is owned or copied.
72///
73/// The lifetime is per call, not per `feed_with`: [`Event::Text`] borrows the
74/// bytes passed in, while sequence payloads borrow the parser's scratch
75/// buffers, which the next sequence reuses. Keep what you need before
76/// returning.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Event<'a> {
79 /// A run of printable bytes, exactly as they arrived: **not** validated as
80 /// UTF-8, and a multi-byte character split across two `feed_with` calls
81 /// arrives as the tail of one run and the head of the next. Callers that
82 /// need `str` decode it themselves; [`Parser::feed`] does that and
83 /// replaces invalid bytes with U+FFFD.
84 Text(&'a [u8]),
85 /// A C0 control byte executed on its own, or DEL.
86 Control(u8),
87 /// A CSI sequence. `params` and `intermediates` borrow parser scratch.
88 Csi {
89 private: Option<u8>,
90 params: &'a [u16],
91 intermediates: &'a [u8],
92 final_byte: u8,
93 },
94 /// A non-CSI escape sequence.
95 Esc {
96 intermediates: &'a [u8],
97 final_byte: u8,
98 },
99 /// An OSC string's payload, without the introducer or terminator.
100 Osc(&'a [u8]),
101 /// A DCS/SOS/PM/APC string's payload.
102 Other { intro: u8, data: &'a [u8] },
103}
104
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
106enum State {
107 #[default]
108 Ground,
109 Escape,
110 EscapeIntermediate,
111 Csi,
112 Osc,
113 Str, // DCS / SOS / PM / APC
114}
115
116/// Cap accumulated sequence payloads so a hostile stream cannot grow memory
117/// without bound; sequences exceeding this are dropped.
118const MAX_SEQ: usize = 4096;
119/// Parameters kept per CSI sequence; later ones are ignored, as terminals do.
120const MAX_PARAMS: usize = 32;
121/// Intermediate bytes kept per sequence.
122const MAX_INTERMEDIATES: usize = 8;
123
124/// The incremental parser. Create one per stream and feed it each chunk as it
125/// arrives, with [`feed_with`](Parser::feed_with) or [`feed`](Parser::feed).
126#[derive(Debug, Default)]
127pub struct Parser {
128 state: State,
129 // CSI scratch, all reused between sequences.
130 private: Option<u8>,
131 params: [u16; MAX_PARAMS],
132 params_len: usize,
133 param_value: u32,
134 param_open: bool,
135 param_sub: bool,
136 param_bytes: usize,
137 intermediates: [u8; MAX_INTERMEDIATES],
138 intermediates_len: usize,
139 // String (OSC/DCS/SOS/PM/APC) scratch.
140 str_intro: u8,
141 str_data: Vec<u8>,
142 str_esc: bool,
143 // Used only by `feed`: a UTF-8 character split across calls.
144 decoder: Utf8Decoder,
145}
146
147impl Parser {
148 /// A parser in the ground state.
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 /// Tokenize `input`, calling `f` once per [`Event`], allocating nothing.
154 ///
155 /// Partial sequences are held for the next call. A text run in progress is
156 /// always delivered before returning, so no input byte is buffered except
157 /// inside an unfinished escape sequence.
158 pub fn feed_with<F: FnMut(Event<'_>)>(&mut self, input: &[u8], mut f: F) {
159 let mut run: Option<usize> = None;
160 let mut i = 0;
161 while i < input.len() {
162 let b = input[i];
163 match self.state {
164 State::Ground => match b {
165 0x1B => {
166 flush_run(&mut run, input, i, &mut f);
167 self.begin(State::Escape);
168 }
169 0x00..=0x1F | 0x7F => {
170 flush_run(&mut run, input, i, &mut f);
171 f(Event::Control(b));
172 }
173 _ => {
174 if run.is_none() {
175 run = Some(i);
176 }
177 }
178 },
179 State::Escape => match b {
180 b'[' => self.begin(State::Csi),
181 b']' => self.begin(State::Osc),
182 b'P' | b'X' | b'^' | b'_' => {
183 self.begin(State::Str);
184 self.str_intro = b;
185 }
186 0x20..=0x2F => {
187 self.push_intermediate(b);
188 self.state = State::EscapeIntermediate;
189 }
190 0x30..=0x7E => {
191 f(Event::Esc {
192 intermediates: &[],
193 final_byte: b,
194 });
195 self.state = State::Ground;
196 }
197 0x1B => self.begin(State::Escape),
198 0x18 | 0x1A => self.state = State::Ground, // CAN / SUB abort
199 _ => f(Event::Control(b)), // execute C0 within sequence
200 },
201 State::EscapeIntermediate => match b {
202 0x20..=0x2F => self.push_intermediate(b),
203 0x30..=0x7E => {
204 f(Event::Esc {
205 intermediates: &self.intermediates[..self.intermediates_len],
206 final_byte: b,
207 });
208 self.state = State::Ground;
209 }
210 0x1B => self.begin(State::Escape),
211 0x18 | 0x1A => self.state = State::Ground,
212 _ => f(Event::Control(b)),
213 },
214 State::Csi => match b {
215 b'0'..=b'9' | b';' | b':' => self.param_byte(b),
216 0x3C..=0x3F => {
217 if self.private.is_none() && self.param_bytes == 0 {
218 self.private = Some(b);
219 } // later private markers are ignored, as most terminals do
220 }
221 0x20..=0x2F => self.push_intermediate(b),
222 0x40..=0x7E => {
223 self.close_param();
224 f(Event::Csi {
225 private: self.private,
226 params: &self.params[..self.params_len],
227 intermediates: &self.intermediates[..self.intermediates_len],
228 final_byte: b,
229 });
230 self.private = None;
231 self.state = State::Ground;
232 }
233 0x1B => self.begin(State::Escape),
234 0x18 | 0x1A => self.state = State::Ground,
235 _ => f(Event::Control(b)),
236 },
237 State::Osc | State::Str => {
238 let is_osc = self.state == State::Osc;
239 if self.str_esc {
240 self.str_esc = false;
241 if b == b'\\' {
242 self.dispatch_string(is_osc, &mut f);
243 i += 1;
244 continue;
245 }
246 self.push_str_byte(0x1B);
247 // fall through: `b` is ordinary payload
248 }
249 match b {
250 0x07 => self.dispatch_string(is_osc, &mut f), // BEL terminator
251 0x1B => self.str_esc = true,
252 _ => self.push_str_byte(b),
253 }
254 }
255 }
256 i += 1;
257 }
258 flush_run(&mut run, input, input.len(), &mut f);
259 }
260
261 /// Feed a chunk of bytes; returns the tokens completed by this chunk.
262 ///
263 /// Any accumulated text run is flushed at the end of the call; partial
264 /// escape sequences and partial UTF-8 characters are held for the next.
265 /// This is [`feed_with`](Parser::feed_with) with the events copied into
266 /// owned [`Token`]s; prefer the engine where the allocation matters.
267 pub fn feed(&mut self, input: &[u8]) -> Vec<Token> {
268 let mut out = Vec::new();
269 let mut text = String::new();
270 let mut decoder = std::mem::take(&mut self.decoder);
271
272 self.feed_with(input, |event| match event {
273 Event::Text(bytes) => decoder.decode(bytes, |s| text.push_str(s)),
274 other => {
275 decoder.flush_incomplete(|s| text.push_str(s));
276 if !text.is_empty() {
277 out.push(Token::Text(std::mem::take(&mut text)));
278 }
279 out.push(own(other));
280 }
281 });
282
283 if !text.is_empty() {
284 out.push(Token::Text(text));
285 }
286 self.decoder = decoder;
287 out
288 }
289
290 fn begin(&mut self, state: State) {
291 self.state = state;
292 self.intermediates_len = 0;
293 self.params_len = 0;
294 self.param_value = 0;
295 self.param_open = false;
296 self.param_sub = false;
297 self.param_bytes = 0;
298 self.private = None;
299 self.str_data.clear();
300 self.str_esc = false;
301 }
302
303 fn push_intermediate(&mut self, b: u8) {
304 if self.intermediates_len < MAX_INTERMEDIATES {
305 self.intermediates[self.intermediates_len] = b;
306 self.intermediates_len += 1;
307 }
308 }
309
310 fn push_str_byte(&mut self, b: u8) {
311 if self.str_data.len() < MAX_SEQ {
312 self.str_data.push(b);
313 }
314 }
315
316 /// One byte of a CSI parameter list: a digit, `;` or `:`.
317 fn param_byte(&mut self, b: u8) {
318 if self.param_bytes >= MAX_SEQ {
319 self.state = State::Ground; // oversized: drop the sequence
320 return;
321 }
322 self.param_bytes += 1;
323 self.param_open = true;
324 match b {
325 b';' => {
326 self.store_param();
327 self.param_value = 0;
328 self.param_sub = false;
329 }
330 b':' => self.param_sub = true, // sub-parameters are truncated
331 _ => {
332 if !self.param_sub {
333 let digit = u32::from(b - b'0');
334 self.param_value = (self.param_value * 10 + digit).min(u16::MAX as u32);
335 }
336 }
337 }
338 }
339
340 fn store_param(&mut self) {
341 if self.params_len < MAX_PARAMS {
342 self.params[self.params_len] = self.param_value as u16;
343 self.params_len += 1;
344 }
345 }
346
347 /// Close the parameter list at the final byte.
348 fn close_param(&mut self) {
349 if self.param_open {
350 self.store_param();
351 self.param_open = false;
352 }
353 }
354
355 fn dispatch_string<F: FnMut(Event<'_>)>(&mut self, is_osc: bool, f: &mut F) {
356 if is_osc {
357 f(Event::Osc(&self.str_data));
358 } else {
359 f(Event::Other {
360 intro: self.str_intro,
361 data: &self.str_data,
362 });
363 }
364 self.str_data.clear();
365 self.state = State::Ground;
366 }
367}
368
369/// Deliver the text run that ends at `end`, if any.
370fn flush_run<F: FnMut(Event<'_>)>(run: &mut Option<usize>, input: &[u8], end: usize, f: &mut F) {
371 if let Some(start) = run.take() {
372 if start < end {
373 f(Event::Text(&input[start..end]));
374 }
375 }
376}
377
378/// Copy a borrowed event into an owned token. Text is handled by the caller,
379/// which decodes UTF-8 across chunk boundaries.
380fn own(event: Event<'_>) -> Token {
381 match event {
382 Event::Text(bytes) => Token::Text(String::from_utf8_lossy(bytes).into_owned()),
383 Event::Control(b) => Token::Control(b),
384 Event::Csi {
385 private,
386 params,
387 intermediates,
388 final_byte,
389 } => Token::Csi {
390 private: private.map(char::from),
391 params: params.to_vec(),
392 intermediates: intermediates.to_vec(),
393 final_byte: char::from(final_byte),
394 },
395 Event::Esc {
396 intermediates,
397 final_byte,
398 } => Token::Esc {
399 intermediates: intermediates.to_vec(),
400 final_byte,
401 },
402 Event::Osc(data) => Token::Osc(String::from_utf8_lossy(data).into_owned()),
403 Event::Other { intro, data } => Token::Other {
404 intro,
405 data: data.to_vec(),
406 },
407 }
408}
409
410/// Decodes [`Event::Text`] bytes into `str` pieces, across chunk boundaries.
411///
412/// [`Event::Text`] is raw bytes, because the parser never copies them. A
413/// consumer that wants characters needs two things this handles: a multi-byte
414/// character split across two `feed_with` calls, and invalid bytes, which
415/// become U+FFFD exactly as [`Parser::feed`] renders them.
416///
417/// Pieces are borrowed: an ASCII run is handed over as one slice of the input
418/// with nothing copied, and only a character straddling a boundary is
419/// assembled in the decoder's own four bytes. It allocates nothing, ever.
420///
421/// ```
422/// # use ansi::{Event, Parser, Utf8Decoder};
423/// let mut parser = Parser::new();
424/// let mut decoder = Utf8Decoder::new();
425/// let mut seen = String::new();
426/// // The 'é' is split across the two chunks.
427/// for chunk in [&b"caf\xC3"[..], &b"\xA9!"[..]] {
428/// parser.feed_with(chunk, |event| match event {
429/// Event::Text(bytes) => decoder.decode(bytes, |s| seen.push_str(s)),
430/// _ => decoder.flush_incomplete(|s| seen.push_str(s)),
431/// });
432/// }
433/// assert_eq!(seen, "café!");
434/// ```
435#[derive(Debug, Default, Clone)]
436pub struct Utf8Decoder {
437 partial: [u8; 4],
438 len: usize,
439}
440
441impl Utf8Decoder {
442 /// A decoder with nothing held over.
443 pub fn new() -> Self {
444 Self::default()
445 }
446
447 /// Decode `bytes`, calling `f` with each piece of text in order.
448 ///
449 /// A character left incomplete at the end is held for the next call.
450 pub fn decode<F: FnMut(&str)>(&mut self, bytes: &[u8], mut f: F) {
451 let mut i = 0;
452 while i < bytes.len() {
453 let b = bytes[i];
454 if self.len > 0 {
455 if (0x80..0xC0).contains(&b) {
456 self.partial[self.len] = b;
457 self.len += 1;
458 i += 1;
459 if self.len == utf8_len(self.partial[0]) {
460 match std::str::from_utf8(&self.partial[..self.len]) {
461 Ok(s) => f(s),
462 Err(_) => f(REPLACEMENT), // overlong, surrogate, out of range
463 }
464 self.len = 0;
465 }
466 continue;
467 }
468 // Truncated sequence: the held bytes were invalid.
469 self.len = 0;
470 f(REPLACEMENT);
471 // fall through and process `b` normally
472 }
473 if b < 0x80 {
474 // An ASCII run is already valid UTF-8: hand it over borrowed.
475 let start = i;
476 while i < bytes.len() && bytes[i] < 0x80 {
477 i += 1;
478 }
479 match std::str::from_utf8(&bytes[start..i]) {
480 Ok(s) => f(s),
481 Err(_) => unreachable!("bytes below 0x80 are valid UTF-8"),
482 }
483 continue;
484 }
485 i += 1;
486 if b < 0xC0 || utf8_len(b) == 1 {
487 f(REPLACEMENT); // stray continuation, or an invalid lead (0xF8+)
488 } else {
489 self.partial[0] = b;
490 self.len = 1;
491 }
492 }
493 }
494
495 /// Give up on a character left incomplete, emitting U+FFFD for it.
496 ///
497 /// Call this when something other than text arrives — a control byte or an
498 /// escape sequence ends a text run, and the held bytes can never be
499 /// completed. Does nothing when no character is pending.
500 pub fn flush_incomplete<F: FnMut(&str)>(&mut self, mut f: F) {
501 if self.len > 0 {
502 self.len = 0;
503 f(REPLACEMENT);
504 }
505 }
506
507 /// True when a partial character is held for the next [`decode`](Self::decode).
508 pub fn is_pending(&self) -> bool {
509 self.len > 0
510 }
511}
512
513const REPLACEMENT: &str = "\u{FFFD}";
514
515/// Expected total length of a UTF-8 sequence from its lead byte (1 for
516/// invalid leads, so they consume exactly themselves).
517fn utf8_len(lead: u8) -> usize {
518 match lead {
519 0xC0..=0xDF => 2,
520 0xE0..=0xEF => 3,
521 0xF0..=0xF7 => 4,
522 _ => 1,
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 fn events(input: &[u8]) -> Vec<Token> {
531 let mut p = Parser::new();
532 let mut out = Vec::new();
533 p.feed_with(input, |e| out.push(own(e)));
534 out
535 }
536
537 #[test]
538 fn engine_and_feed_agree_on_every_chunk_split() {
539 let corpus: &[&[u8]] = &[
540 b"hello world\r\n",
541 b"\x1b[1;31mred\x1b[0m",
542 b"\x1b[?25l\x1b[>4;2m\x1b[38;2;10;20;30mx",
543 b"\x1b]0;title\x07after\x1b]133;A\x1b\\done",
544 b"\x1bPq#0;2\x1b\\tail",
545 b"\x1b7\x1b(B\x1b[12\x18aborted",
546 "caf\u{e9} \u{1F600} \u{4e2d}\u{6587}".as_bytes(),
547 &[0xE2, 0x82, b'x', 0xFF, 0x80, b'y'],
548 ];
549 for input in corpus {
550 let whole = {
551 let mut p = Parser::new();
552 p.feed(input)
553 };
554 for split in 1..input.len() {
555 let mut p = Parser::new();
556 let mut got = p.feed(&input[..split]);
557 got.extend(p.feed(&input[split..]));
558 assert_eq!(merge_text(&got), merge_text(&whole), "split at {split}");
559 }
560 }
561 }
562
563 /// Adjacent text tokens are equivalent to one, so compare merged runs.
564 fn merge_text(tokens: &[Token]) -> Vec<Token> {
565 let mut out: Vec<Token> = Vec::new();
566 for t in tokens {
567 match (out.last_mut(), t) {
568 (Some(Token::Text(prev)), Token::Text(next)) => prev.push_str(next),
569 _ => out.push(t.clone()),
570 }
571 }
572 out
573 }
574
575 #[test]
576 fn text_events_borrow_the_input_verbatim() {
577 let mut p = Parser::new();
578 let mut runs: Vec<Vec<u8>> = Vec::new();
579 p.feed_with(b"ab\x1b[0mcd\xFF", |e| {
580 if let Event::Text(b) = e {
581 runs.push(b.to_vec());
582 }
583 });
584 // Raw bytes, including the invalid 0xFF, and no U+FFFD substitution.
585 assert_eq!(runs, vec![b"ab".to_vec(), b"cd\xFF".to_vec()]);
586 }
587
588 #[test]
589 fn parameters_match_the_owned_api() {
590 assert_eq!(
591 events(b"\x1b[m"),
592 vec![Token::Csi {
593 private: None,
594 params: vec![],
595 intermediates: vec![],
596 final_byte: 'm'
597 }]
598 );
599 assert_eq!(
600 events(b"\x1b[;1;;2m"),
601 vec![Token::Csi {
602 private: None,
603 params: vec![0, 1, 0, 2],
604 intermediates: vec![],
605 final_byte: 'm'
606 }]
607 );
608 // Colon sub-parameters are truncated, not split.
609 assert_eq!(
610 events(b"\x1b[38:2:10:20:30;1m"),
611 vec![Token::Csi {
612 private: None,
613 params: vec![38, 1],
614 intermediates: vec![],
615 final_byte: 'm'
616 }]
617 );
618 // Values saturate at u16::MAX.
619 assert_eq!(
620 events(b"\x1b[999999m"),
621 vec![Token::Csi {
622 private: None,
623 params: vec![u16::MAX],
624 intermediates: vec![],
625 final_byte: 'm'
626 }]
627 );
628 // Only the first private marker counts, and only before parameters.
629 assert_eq!(
630 events(b"\x1b[?>25h"),
631 vec![Token::Csi {
632 private: Some('?'),
633 params: vec![25],
634 intermediates: vec![],
635 final_byte: 'h'
636 }]
637 );
638 }
639
640 #[test]
641 fn more_than_max_params_are_ignored_not_dropped() {
642 let mut input = b"\x1b[".to_vec();
643 for i in 0..40 {
644 if i > 0 {
645 input.push(b';');
646 }
647 input.extend_from_slice(b"7");
648 }
649 input.push(b'm');
650 let Token::Csi { params, .. } = &events(&input)[0] else {
651 panic!("expected a CSI");
652 };
653 assert_eq!(params.len(), MAX_PARAMS);
654 assert!(params.iter().all(|&p| p == 7));
655 }
656
657 #[test]
658 fn oversized_parameter_lists_drop_the_sequence() {
659 let mut input = b"\x1b[".to_vec();
660 input.extend(std::iter::repeat(b'1').take(MAX_SEQ + 8));
661 input.push(b'm');
662 // Dropped: nothing is emitted as a CSI, and the tail returns to ground.
663 let tokens = events(&input);
664 assert!(!tokens.iter().any(|t| matches!(t, Token::Csi { .. })));
665 }
666
667 #[test]
668 fn strings_cap_their_payload() {
669 let mut input = b"\x1b]0;".to_vec();
670 input.extend(std::iter::repeat(b'x').take(MAX_SEQ + 100));
671 input.push(0x07);
672 let Token::Osc(payload) = &events(&input)[0] else {
673 panic!("expected an OSC");
674 };
675 assert_eq!(payload.len(), MAX_SEQ);
676 }
677
678 #[test]
679 fn scratch_is_reused_across_sequences() {
680 let mut p = Parser::new();
681 let mut seen = Vec::new();
682 p.feed_with(b"\x1b[1;2m\x1b[?7h\x1b]0;t\x07", |e| seen.push(own(e)));
683 assert_eq!(seen.len(), 3);
684 // A second sequence must not inherit the first one's parameters,
685 // private marker or payload.
686 assert_eq!(
687 seen[1],
688 Token::Csi {
689 private: Some('?'),
690 params: vec![7],
691 intermediates: vec![],
692 final_byte: 'h'
693 }
694 );
695 assert_eq!(seen[2], Token::Osc("0;t".into()));
696 }
697
698 #[test]
699 fn a_partial_sequence_survives_the_chunk_boundary() {
700 let mut p = Parser::new();
701 let mut seen = Vec::new();
702 for chunk in [&b"\x1b"[..], &b"[1"[..], &b";2"[..], &b"H"[..]] {
703 p.feed_with(chunk, |e| seen.push(own(e)));
704 }
705 assert_eq!(
706 seen,
707 vec![Token::Csi {
708 private: None,
709 params: vec![1, 2],
710 intermediates: vec![],
711 final_byte: 'H'
712 }]
713 );
714 }
715}