@@ -4,32 +4,25 @@ import Testing
44
55struct ANSIParserTests {
66
7- // MARK: - Helper
8-
9- /// Shorthand to parse a string and return actions.
107 private func parse( _ input: String ) -> [ TerminalAction ] {
118 var parser = ANSIParser ( )
129 return parser. parse ( input)
1310 }
1411
15- /// Extract the first .text action's span, if any.
1612 private func firstSpan( _ actions: [ TerminalAction ] ) -> StyledSpan ? {
1713 for action in actions {
1814 if case . text( let span) = action { return span }
1915 }
2016 return nil
2117 }
2218
23- /// Collect all .text spans from actions.
2419 private func allSpans( _ actions: [ TerminalAction ] ) -> [ StyledSpan ] {
2520 actions. compactMap {
2621 if case . text( let span) = $0 { return span }
2722 return nil
2823 }
2924 }
3025
31- // MARK: - Plain text
32-
3326 @Test func plainTextPassesThrough( ) {
3427 let actions = parse ( " hello world " )
3528 #expect( actions. count == 1 )
@@ -46,8 +39,6 @@ struct ANSIParserTests {
4639 #expect( actions. isEmpty)
4740 }
4841
49- // MARK: - Newline / carriage return
50-
5142 @Test func newlineProducesNewlineAction( ) {
5243 let actions = parse ( " \n " )
5344 #expect( actions. count == 1 )
@@ -66,17 +57,6 @@ struct ANSIParserTests {
6657 }
6758 }
6859
69- @Test func crlfGraphemeClusterIsFiltered( ) {
70- // In Swift, \r\n forms a single Character grapheme cluster that does NOT
71- // match case "\r" or case "\n" in the parser's switch. Its asciiValue is 10
72- // (newline), which is < 32 and != 9, so it gets filtered as a control char.
73- // This is a known limitation of character-level parsing in Swift.
74- let input = String ( Unicode . Scalar ( 0x0D ) ) + String( Unicode . Scalar ( 0x0A ) )
75- let actions = parse ( input)
76- // CRLF grapheme is filtered out, producing no actions
77- #expect( actions. isEmpty)
78- }
79-
8060 @Test func textBeforeAndAfterNewline( ) {
8161 let actions = parse ( " abc \n def " )
8262 #expect( actions. count == 3 )
@@ -88,8 +68,6 @@ struct ANSIParserTests {
8868 if case . text( let s2) = actions [ 2 ] { #expect( s2. text == " def " ) }
8969 }
9070
91- // MARK: - Standard foreground colors (30-37)
92-
9371 @Test func standardForegroundColors( ) {
9472 for code in 30 ... 37 {
9573 let actions = parse ( " \u{1B} [ \( code) mX " )
@@ -99,8 +77,6 @@ struct ANSIParserTests {
9977 }
10078 }
10179
102- // MARK: - Standard background colors (40-47)
103-
10480 @Test func standardBackgroundColors( ) {
10581 for code in 40 ... 47 {
10682 let actions = parse ( " \u{1B} [ \( code) mX " )
@@ -110,8 +86,6 @@ struct ANSIParserTests {
11086 }
11187 }
11288
113- // MARK: - Bright foreground colors (90-97)
114-
11589 @Test func brightForegroundColors( ) {
11690 for code in 90 ... 97 {
11791 let actions = parse ( " \u{1B} [ \( code) mX " )
@@ -121,8 +95,6 @@ struct ANSIParserTests {
12195 }
12296 }
12397
124- // MARK: - Bright background colors (100-107)
125-
12698 @Test func brightBackgroundColors( ) {
12799 for code in 100 ... 107 {
128100 let actions = parse ( " \u{1B} [ \( code) mX " )
@@ -132,8 +104,6 @@ struct ANSIParserTests {
132104 }
133105 }
134106
135- // MARK: - 256-color palette
136-
137107 @Test func foreground256Color( ) {
138108 let actions = parse ( " \u{1B} [38;5;123mHi " )
139109 let span = firstSpan ( actions)
@@ -160,8 +130,6 @@ struct ANSIParserTests {
160130 #expect( span? . style. foreground == . palette( 255 ) )
161131 }
162132
163- // MARK: - RGB truecolor
164-
165133 @Test func foregroundRGBColor( ) {
166134 let actions = parse ( " \u{1B} [38;2;255;128;0mTruecolor " )
167135 let span = firstSpan ( actions)
@@ -188,8 +156,6 @@ struct ANSIParserTests {
188156 #expect( span? . style. foreground == . rgb( 255 , 255 , 255 ) )
189157 }
190158
191- // MARK: - Text styles
192-
193159 @Test func boldStyle( ) {
194160 let actions = parse ( " \u{1B} [1mBold " )
195161 let span = firstSpan ( actions)
@@ -221,13 +187,9 @@ struct ANSIParserTests {
221187 #expect( span? . style. strikethrough == true )
222188 }
223189
224- // MARK: - Reset
225-
226190 @Test func resetClearsAllStyles( ) {
227191 var parser = ANSIParser ( )
228- // Set bold + red foreground
229192 _ = parser. parse ( " \u{1B} [1;31m " )
230- // Now reset
231193 let actions = parser. parse ( " \u{1B} [0mAfterReset " )
232194 let span = firstSpan ( actions)
233195 #expect( span? . style == ANSIStyle ( ) )
@@ -242,8 +204,6 @@ struct ANSIParserTests {
242204 #expect( span? . style == ANSIStyle ( ) )
243205 }
244206
245- // MARK: - Individual style resets
246-
247207 @Test func code22ResetsBoldAndDim( ) {
248208 var parser = ANSIParser ( )
249209 _ = parser. parse ( " \u{1B} [1;2m " )
@@ -277,8 +237,6 @@ struct ANSIParserTests {
277237 #expect( span? . style. strikethrough == false )
278238 }
279239
280- // MARK: - Default foreground / background
281-
282240 @Test func code39ResetsDefaultForeground( ) {
283241 var parser = ANSIParser ( )
284242 _ = parser. parse ( " \u{1B} [31m " )
@@ -295,8 +253,6 @@ struct ANSIParserTests {
295253 #expect( span? . style. background == . default)
296254 }
297255
298- // MARK: - Erase line
299-
300256 @Test func eraseLineCode2K( ) {
301257 let actions = parse ( " \u{1B} [2K " )
302258 #expect( actions. count == 1 )
@@ -324,8 +280,6 @@ struct ANSIParserTests {
324280 }
325281 }
326282
327- // MARK: - Cursor up
328-
329283 @Test func cursorUpDefault( ) {
330284 let actions = parse ( " \u{1B} [A " )
331285 #expect( actions. count == 1 )
@@ -355,28 +309,23 @@ struct ANSIParserTests {
355309 #expect( n == 1 )
356310 }
357311
358- // MARK: - Multiple SGR params in one sequence
359-
360312 @Test func multipleSGRParams( ) {
361313 let actions = parse ( " \u{1B} [1;31;42mCombined " )
362314 let span = firstSpan ( actions)
363315 #expect( span? . style. bold == true )
364- #expect( span? . style. foreground == . standard( 1 ) ) // red
365- #expect( span? . style. background == . standard( 2 ) ) // green
316+ #expect( span? . style. foreground == . standard( 1 ) )
317+ #expect( span? . style. background == . standard( 2 ) )
366318 #expect( span? . text == " Combined " )
367319 }
368320
369321 @Test func multipleSGRParamsItalicBrightCyan( ) {
370322 let actions = parse ( " \u{1B} [3;96mTest " )
371323 let span = firstSpan ( actions)
372324 #expect( span? . style. italic == true )
373- #expect( span? . style. foreground == . bright( 6 ) ) // bright cyan
325+ #expect( span? . style. foreground == . bright( 6 ) )
374326 }
375327
376- // MARK: - Control characters
377-
378328 @Test func controlCharactersAreFiltered( ) {
379- // Characters below 32 (except tab) should be stripped
380329 let input = " A \u{01} B \u{02} C \u{07} D "
381330 let actions = parse ( input)
382331 let span = firstSpan ( actions)
@@ -389,37 +338,29 @@ struct ANSIParserTests {
389338 #expect( span? . text == " A \t B " )
390339 }
391340
392- // MARK: - Incomplete / malformed sequences
393-
394341 @Test func incompleteEscapeSequenceIsSkipped( ) {
395- // ESC followed by end of string
396342 let actions = parse ( " Hello \u{1B} " )
397343 #expect( actions. count == 1 )
398344 let span = firstSpan ( actions)
399345 #expect( span? . text == " Hello " )
400346 }
401347
402348 @Test func escNotFollowedByBracketSkips( ) {
403- // ESC followed by non-bracket char
404349 let actions = parse ( " A \u{1B} XB " )
405- // "A" is flushed before ESC, ESC+X advances past ESC, then "XB" continues
406350 let spans = allSpans ( actions)
407351 let combined = spans. map ( \. text) . joined ( )
408352 #expect( combined == " AXB " )
409353 }
410354
411355 @Test func incompleteCSISequenceIsSkipped( ) {
412- // ESC[ with digits but no final letter
413356 let actions = parse ( " Hi \u{1B} [31 " )
414357 let span = firstSpan ( actions)
415358 #expect( span? . text == " Hi " )
416359 }
417360
418- // MARK: - Style persistence across parse calls
419-
420361 @Test func stylePersistsAcrossParseCalls( ) {
421362 var parser = ANSIParser ( )
422- _ = parser. parse ( " \u{1B} [1;31m " ) // bold + red
363+ _ = parser. parse ( " \u{1B} [1;31m " )
423364 let actions = parser. parse ( " StyledText " )
424365 let span = firstSpan ( actions)
425366 #expect( span? . style. bold == true )
@@ -438,8 +379,6 @@ struct ANSIParserTests {
438379 #expect( s2? . style == ANSIStyle ( ) )
439380 }
440381
441- // MARK: - Mixed text and escapes
442-
443382 @Test func mixedTextAndEscapes( ) {
444383 let actions = parse ( " Hello \u{1B} [31mWorld \u{1B} [0m! " )
445384 let spans = allSpans ( actions)
@@ -477,29 +416,16 @@ struct ANSIParserTests {
477416 }
478417 }
479418
480- @Test func textWithCRLFGraphemeClusters( ) {
481- // CRLF grapheme clusters are filtered (see crlfGraphemeClusterIsFiltered),
482- // so text on either side merges into one span.
483- let crlf = String ( Unicode . Scalar ( 0x0D ) ) + String( Unicode . Scalar ( 0x0A ) )
484- let input = " line1 " + crlf + " line2 "
485- let actions = parse ( input)
486- #expect( actions. count == 1 )
487- if case . text( let s) = actions [ 0 ] { #expect( s. text == " line1line2 " ) }
488- }
489-
490419 @Test func separateCRAndLFProduceBothActions( ) {
491- // When \r and \n arrive in separate parse() calls, they work as expected
492420 var parser = ANSIParser ( )
493421 let a1 = parser. parse ( " line1 \r " )
494422 let a2 = parser. parse ( " \n line2 " )
495- // First parse: text("line1") + carriageReturn
496423 #expect( a1. count == 2 )
497424 if case . text( let s) = a1 [ 0 ] { #expect( s. text == " line1 " ) }
498425 guard case . carriageReturn = a1 [ 1 ] else {
499426 Issue . record ( " Expected .carriageReturn " )
500427 return
501428 }
502- // Second parse: newline + text("line2")
503429 #expect( a2. count == 2 )
504430 guard case . newline = a2 [ 0 ] else {
505431 Issue . record ( " Expected .newline " )
@@ -519,60 +445,44 @@ struct ANSIParserTests {
519445 if case . text( let s) = actions [ 2 ] { #expect( s. text == " new " ) }
520446 }
521447
522- // MARK: - Extended color edge cases
523-
524448 @Test func extendedColor38WithInsufficientParams( ) {
525- // 38;5 without the color number -- should not crash
526449 let actions = parse ( " \u{1B} [38;5mX " )
527450 let span = firstSpan ( actions)
528- // Parser should still produce text, foreground stays default
529451 #expect( span? . text == " X " )
530452 }
531453
532454 @Test func extendedColor38ModeUnknown( ) {
533- // 38;3 is not a recognized mode (only 2 and 5)
534455 let actions = parse ( " \u{1B} [38;3;100mX " )
535456 let span = firstSpan ( actions)
536457 #expect( span? . text == " X " )
537- // foreground should remain default since mode 3 is unrecognized
538458 #expect( span? . style. foreground == . default)
539459 }
540460
541461 @Test func truecolorWithInsufficientParams( ) {
542- // 38;2;255;128 is missing the blue component
543462 let actions = parse ( " \u{1B} [38;2;255;128mX " )
544463 let span = firstSpan ( actions)
545464 #expect( span? . text == " X " )
546- // Should not crash; foreground stays default since insufficient params
547465 #expect( span? . style. foreground == . default)
548466 }
549467
550- // MARK: - Unrecognized CSI final characters
551-
552468 @Test func unrecognizedCSIFinalCharIsIgnored( ) {
553- // ESC[5J is a valid CSI form but 'J' (erase display) isn't handled
554469 let actions = parse ( " A \u{1B} [2JB " )
555470 let spans = allSpans ( actions)
556471 let combined = spans. map ( \. text) . joined ( )
557472 #expect( combined == " AB " )
558473 }
559474
560- // MARK: - Complex real-world sequences
561-
562475 @Test func npmColoredOutput( ) {
563- // Simulate typical npm output with reset, bold, colors
564476 let input = " \u{1B} [1m \u{1B} [32m> \u{1B} [0m dev \n next dev "
565477 let actions = parse ( input)
566478 let spans = allSpans ( actions)
567479 #expect( spans. count >= 2 )
568- // First span should be bold + green ">"
569480 #expect( spans [ 0 ] . style. bold == true )
570481 #expect( spans [ 0 ] . style. foreground == . standard( 2 ) )
571482 #expect( spans [ 0 ] . text == " > " )
572483 }
573484
574485 @Test func progressBarWithCR( ) {
575- // Simulate a progress bar that uses carriage return to overwrite
576486 let input = " Progress: 50% \r Progress: 100% "
577487 let actions = parse ( input)
578488 #expect( actions. count == 3 )
0 commit comments