@@ -6,6 +6,142 @@ use core::{
66 str:: CharIndices ,
77} ;
88
9+ #[ derive( Clone , Copy , Debug ) ]
10+ struct OscSequence ;
11+
12+ impl EscSequence for OscSequence {
13+ const START : char = ']' ;
14+
15+ fn on_escape ( next : Option < char > ) -> EscAction {
16+ if matches ! ( next, Some ( '\\' ) ) {
17+ EscAction {
18+ consume_next : true ,
19+ end : true ,
20+ }
21+ } else {
22+ EscAction {
23+ consume_next : false ,
24+ end : false ,
25+ }
26+ }
27+ }
28+
29+ fn on_char ( c : char ) -> EscAction {
30+ EscAction {
31+ consume_next : false ,
32+ end : c == '\u{07}' ,
33+ }
34+ }
35+ }
36+
37+ #[ derive( Clone , Copy , Debug ) ]
38+ struct DcsSequence ;
39+
40+ impl EscSequence for DcsSequence {
41+ const START : char = 'P' ;
42+
43+ fn on_escape ( next : Option < char > ) -> EscAction {
44+ match next {
45+ Some ( '\\' ) => EscAction {
46+ consume_next : true ,
47+ end : true ,
48+ } ,
49+ None => EscAction {
50+ consume_next : false ,
51+ end : true ,
52+ } ,
53+ Some ( '\u{1b}' ) => EscAction {
54+ consume_next : true ,
55+ end : false ,
56+ } ,
57+ _ => EscAction {
58+ consume_next : false ,
59+ end : false ,
60+ } ,
61+ }
62+ }
63+
64+ fn on_char ( _c : char ) -> EscAction {
65+ EscAction {
66+ consume_next : false ,
67+ end : false ,
68+ }
69+ }
70+ }
71+
72+ trait EscSequence {
73+ fn on_escape ( next : Option < char > ) -> EscAction ;
74+ fn on_char ( c : char ) -> EscAction ;
75+ const START : char ;
76+ }
77+
78+ struct EscAction {
79+ consume_next : bool ,
80+ end : bool ,
81+ }
82+
83+ fn consume_end_exclusive < S : EscSequence > (
84+ it : & mut Peekable < CharIndices < ' _ > > ,
85+ start : usize ,
86+ ) -> usize {
87+ let mut end = start + 1 ;
88+ let Some ( ( idx, start_char) ) = it. next ( ) else {
89+ return end;
90+ } ;
91+ if start_char != S :: START {
92+ return end;
93+ }
94+ end = idx + 1 ;
95+
96+ while let Some ( ( idx, c) ) = it. next ( ) {
97+ end = idx + c. len_utf8 ( ) ;
98+ match c {
99+ '\u{9c}' => return end,
100+ '\u{1b}' => {
101+ let action = S :: on_escape ( it. peek ( ) . map ( |( _, next) | * next) ) ;
102+ if action. consume_next {
103+ if let Some ( ( next_idx, _) ) = it. peek ( ) {
104+ end = * next_idx + 1 ;
105+ it. next ( ) ;
106+ }
107+ }
108+ if action. end {
109+ return end;
110+ }
111+ }
112+ _ => { }
113+ }
114+ if S :: on_char ( c) . end {
115+ return end;
116+ }
117+ }
118+
119+ end
120+ }
121+
122+ fn find_dfa_end_exclusive_after_entry ( it : & mut Peekable < CharIndices < ' _ > > ) -> Option < usize > {
123+ let mut state = State :: S1 ;
124+ let mut maybe_end = None ;
125+
126+ loop {
127+ let item = it. peek ( ) ;
128+
129+ if let Some ( ( idx, c) ) = item {
130+ state. transition ( * c) ;
131+
132+ if state. is_final ( ) {
133+ maybe_end = Some ( * idx) ;
134+ }
135+ }
136+
137+ if state. is_trapped ( ) || item. is_none ( ) {
138+ return maybe_end. map ( |end| end + 1 ) ;
139+ }
140+
141+ it. next ( ) ;
142+ }
143+ }
144+
9145#[ derive( Debug , Clone , Copy , Default ) ]
10146enum State {
11147 #[ default]
@@ -143,42 +279,36 @@ impl FusedIterator for Matches<'_> {}
143279
144280fn find_ansi_code_exclusive ( it : & mut Peekable < CharIndices > ) -> Option < ( usize , usize ) > {
145281 ' outer: loop {
146- if let ( start, '\u{1b}' ) | ( start, '\u{9b}' ) = it. peek ( ) ? {
147- let start = * start;
148- let mut state = State :: default ( ) ;
149- let mut maybe_end = None ;
150-
151- loop {
152- let item = it. peek ( ) ;
153-
154- if let Some ( ( idx, c) ) = item {
155- state. transition ( * c) ;
156-
157- if state. is_final ( ) {
158- maybe_end = Some ( * idx) ;
282+ let ( start, entry) = * it. peek ( ) ?;
283+ match entry {
284+ '\u{1b}' => {
285+ it. next ( ) ;
286+ match it. peek ( ) {
287+ Some ( ( _, OscSequence :: START ) ) => {
288+ return Some ( ( start, consume_end_exclusive :: < OscSequence > ( it, start) ) )
159289 }
160- }
161-
162- // The match is greedy so run till we hit the trap state no matter what. A valid
163- // match is just one that was final at some point
164- if state. is_trapped ( ) || item. is_none ( ) {
165- match maybe_end {
166- Some ( end) => {
167- // All possible final characters are a single byte so it's safe to make
168- // the end exclusive by just adding one
169- return Some ( ( start, end + 1 ) ) ;
290+ Some ( ( _, DcsSequence :: START ) ) => {
291+ return Some ( ( start, consume_end_exclusive :: < DcsSequence > ( it, start) ) )
292+ }
293+ _ => {
294+ if let Some ( end) = find_dfa_end_exclusive_after_entry ( it) {
295+ return Some ( ( start, end) ) ;
170296 }
171- // The character we are peeking right now might be the start of a match so
172- // we want to continue the loop without popping off that char
173- None => continue ' outer,
297+ continue ' outer;
174298 }
175299 }
176-
300+ }
301+ '\u{9b}' => {
302+ it. next ( ) ;
303+ if let Some ( end) = find_dfa_end_exclusive_after_entry ( it) {
304+ return Some ( ( start, end) ) ;
305+ }
306+ continue ' outer;
307+ }
308+ _ => {
177309 it. next ( ) ;
178310 }
179311 }
180-
181- it. next ( ) ;
182312 }
183313}
184314
@@ -296,12 +426,16 @@ mod tests {
296426 use proptest:: prelude:: * ;
297427 use regex:: Regex ;
298428
299- // The manual dfa `State` is a handwritten translation from the previously used regex. That
300- // regex is kept here and used to ensure that the new matches are the same as the old
429+ // The manual dfa `State` is a handwritten translation of the following regex.
301430 static STRIP_ANSI_RE : Lazy < Regex > = Lazy :: new ( || {
302- Regex :: new (
303- r"[\x1b\x9b]([()][012AB]|[\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><])" ,
304- )
431+ Regex :: new ( concat ! (
432+ r"(?s)(?:" ,
433+ r"\x1b\].*?(?:\x07|\x9c|\x1b\\|\z)|" ,
434+ r"\x1bP(?:[^\x1b\x9c]|\x1b\x1b|\x1b[^\x1b\\])*?(?:\x9c|\x1b\\|\x1b\z|\z)|" ,
435+ r"[\x1b\x9b]([()][012AB]|[\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?" ,
436+ r"[0-9A-PRZcf-nqry=><])" ,
437+ r")" ,
438+ ) )
305439 . unwrap ( )
306440 } ) ;
307441
@@ -415,6 +549,28 @@ mod tests {
415549 }
416550 }
417551
552+ #[ test]
553+ fn strip_osc8_hyperlink_st ( ) {
554+ let s = "\x1b ]8;;file:///tmp/test\x1b \\ hello\x1b ]8;;\x1b \\ " ;
555+ assert_eq ! ( strip_ansi_codes( s) . as_ref( ) , "hello" ) ;
556+ }
557+
558+ #[ test]
559+ fn strip_osc8_hyperlink_bel ( ) {
560+ let s = "\x1b ]8;;file:///tmp/test\x07 hello\x1b ]8;;\x07 " ;
561+ assert_eq ! ( strip_ansi_codes( s) . as_ref( ) , "hello" ) ;
562+ }
563+
564+ #[ test]
565+ fn strip_tmux_passthrough_dcs ( ) {
566+ let open = "\x1b Ptmux;\x1b \x1b \x1b ]8;;file:///tmp/test\x1b \x1b \\ \x1b \\ " ;
567+ let close = "\x1b Ptmux;\x1b \x1b \x1b ]8;;\x1b \x1b \\ \x1b \\ " ;
568+ assert_eq ! (
569+ strip_ansi_codes( & format!( "{open}hello{close}" ) ) . as_ref( ) ,
570+ "hello"
571+ ) ;
572+ }
573+
418574 #[ test]
419575 fn test_ansi_iter_re_vt100 ( ) {
420576 let s = "\x1b (0lpq\x1b )Benglish" ;
0 commit comments