command-stream 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
package/js/src/shell-parser.mjs
CHANGED
|
@@ -22,6 +22,119 @@ const TokenType = {
|
|
|
22
22
|
EOF: 'eof',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parse a word token from the command string, handling quotes and escapes
|
|
27
|
+
* @param {string} command - The command string
|
|
28
|
+
* @param {number} startIndex - Starting position
|
|
29
|
+
* @returns {{word: string, endIndex: number}} Parsed word and end position
|
|
30
|
+
*/
|
|
31
|
+
function parseWord(command, startIndex) {
|
|
32
|
+
let word = '';
|
|
33
|
+
let i = startIndex;
|
|
34
|
+
let inQuote = false;
|
|
35
|
+
let quoteChar = '';
|
|
36
|
+
|
|
37
|
+
while (i < command.length) {
|
|
38
|
+
const char = command[i];
|
|
39
|
+
|
|
40
|
+
if (!inQuote) {
|
|
41
|
+
const result = parseUnquotedChar(command, i, char, word);
|
|
42
|
+
if (result.done) {
|
|
43
|
+
return { word: result.word, endIndex: i };
|
|
44
|
+
}
|
|
45
|
+
word = result.word;
|
|
46
|
+
i = result.index;
|
|
47
|
+
if (result.startQuote) {
|
|
48
|
+
inQuote = true;
|
|
49
|
+
quoteChar = result.startQuote;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
const result = parseQuotedChar(command, i, char, word, quoteChar);
|
|
53
|
+
word = result.word;
|
|
54
|
+
i = result.index;
|
|
55
|
+
if (result.endQuote) {
|
|
56
|
+
inQuote = false;
|
|
57
|
+
quoteChar = '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { word, endIndex: i };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse a character when not inside quotes
|
|
67
|
+
*/
|
|
68
|
+
function parseUnquotedChar(command, i, char, word) {
|
|
69
|
+
if (char === '"' || char === "'") {
|
|
70
|
+
return { word: word + char, index: i + 1, startQuote: char };
|
|
71
|
+
}
|
|
72
|
+
if (/\s/.test(char) || '&|;()<>'.includes(char)) {
|
|
73
|
+
return { word, done: true };
|
|
74
|
+
}
|
|
75
|
+
if (char === '\\' && i + 1 < command.length) {
|
|
76
|
+
const escaped = command[i + 1];
|
|
77
|
+
return { word: word + char + escaped, index: i + 2 };
|
|
78
|
+
}
|
|
79
|
+
return { word: word + char, index: i + 1 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a character when inside quotes
|
|
84
|
+
*/
|
|
85
|
+
function parseQuotedChar(command, i, char, word, quoteChar) {
|
|
86
|
+
if (char === quoteChar && command[i - 1] !== '\\') {
|
|
87
|
+
return { word: word + char, index: i + 1, endQuote: true };
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
char === '\\' &&
|
|
91
|
+
i + 1 < command.length &&
|
|
92
|
+
(command[i + 1] === quoteChar || command[i + 1] === '\\')
|
|
93
|
+
) {
|
|
94
|
+
const escaped = command[i + 1];
|
|
95
|
+
return { word: word + char + escaped, index: i + 2 };
|
|
96
|
+
}
|
|
97
|
+
return { word: word + char, index: i + 1 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Try to match an operator at the current position
|
|
102
|
+
* @returns {{type: string, value: string, length: number} | null}
|
|
103
|
+
*/
|
|
104
|
+
function matchOperator(command, i) {
|
|
105
|
+
const twoChar = command.slice(i, i + 2);
|
|
106
|
+
const oneChar = command[i];
|
|
107
|
+
|
|
108
|
+
if (twoChar === '&&') {
|
|
109
|
+
return { type: TokenType.AND, value: '&&', length: 2 };
|
|
110
|
+
}
|
|
111
|
+
if (twoChar === '||') {
|
|
112
|
+
return { type: TokenType.OR, value: '||', length: 2 };
|
|
113
|
+
}
|
|
114
|
+
if (twoChar === '>>') {
|
|
115
|
+
return { type: TokenType.REDIRECT_APPEND, value: '>>', length: 2 };
|
|
116
|
+
}
|
|
117
|
+
if (oneChar === '|') {
|
|
118
|
+
return { type: TokenType.PIPE, value: '|', length: 1 };
|
|
119
|
+
}
|
|
120
|
+
if (oneChar === ';') {
|
|
121
|
+
return { type: TokenType.SEMICOLON, value: ';', length: 1 };
|
|
122
|
+
}
|
|
123
|
+
if (oneChar === '(') {
|
|
124
|
+
return { type: TokenType.LPAREN, value: '(', length: 1 };
|
|
125
|
+
}
|
|
126
|
+
if (oneChar === ')') {
|
|
127
|
+
return { type: TokenType.RPAREN, value: ')', length: 1 };
|
|
128
|
+
}
|
|
129
|
+
if (oneChar === '>') {
|
|
130
|
+
return { type: TokenType.REDIRECT_OUT, value: '>', length: 1 };
|
|
131
|
+
}
|
|
132
|
+
if (oneChar === '<') {
|
|
133
|
+
return { type: TokenType.REDIRECT_IN, value: '<', length: 1 };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
25
138
|
/**
|
|
26
139
|
* Tokenize a shell command string
|
|
27
140
|
*/
|
|
@@ -40,90 +153,19 @@ function tokenize(command) {
|
|
|
40
153
|
}
|
|
41
154
|
|
|
42
155
|
// Check for operators
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} else if (command[i] === '|') {
|
|
50
|
-
tokens.push({ type: TokenType.PIPE, value: '|' });
|
|
51
|
-
i++;
|
|
52
|
-
} else if (command[i] === ';') {
|
|
53
|
-
tokens.push({ type: TokenType.SEMICOLON, value: ';' });
|
|
54
|
-
i++;
|
|
55
|
-
} else if (command[i] === '(') {
|
|
56
|
-
tokens.push({ type: TokenType.LPAREN, value: '(' });
|
|
57
|
-
i++;
|
|
58
|
-
} else if (command[i] === ')') {
|
|
59
|
-
tokens.push({ type: TokenType.RPAREN, value: ')' });
|
|
60
|
-
i++;
|
|
61
|
-
} else if (command[i] === '>' && command[i + 1] === '>') {
|
|
62
|
-
tokens.push({ type: TokenType.REDIRECT_APPEND, value: '>>' });
|
|
63
|
-
i += 2;
|
|
64
|
-
} else if (command[i] === '>') {
|
|
65
|
-
tokens.push({ type: TokenType.REDIRECT_OUT, value: '>' });
|
|
66
|
-
i++;
|
|
67
|
-
} else if (command[i] === '<') {
|
|
68
|
-
tokens.push({ type: TokenType.REDIRECT_IN, value: '<' });
|
|
69
|
-
i++;
|
|
70
|
-
} else {
|
|
71
|
-
// Parse word (respecting quotes)
|
|
72
|
-
let word = '';
|
|
73
|
-
let inQuote = false;
|
|
74
|
-
let quoteChar = '';
|
|
75
|
-
|
|
76
|
-
while (i < command.length) {
|
|
77
|
-
const char = command[i];
|
|
78
|
-
|
|
79
|
-
if (!inQuote) {
|
|
80
|
-
if (char === '"' || char === "'") {
|
|
81
|
-
inQuote = true;
|
|
82
|
-
quoteChar = char;
|
|
83
|
-
word += char;
|
|
84
|
-
i++;
|
|
85
|
-
} else if (/\s/.test(char) || '&|;()<>'.includes(char)) {
|
|
86
|
-
break;
|
|
87
|
-
} else if (char === '\\' && i + 1 < command.length) {
|
|
88
|
-
// Handle escape sequences
|
|
89
|
-
word += char;
|
|
90
|
-
i++;
|
|
91
|
-
if (i < command.length) {
|
|
92
|
-
word += command[i];
|
|
93
|
-
i++;
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
word += char;
|
|
97
|
-
i++;
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
if (char === quoteChar && command[i - 1] !== '\\') {
|
|
101
|
-
inQuote = false;
|
|
102
|
-
quoteChar = '';
|
|
103
|
-
word += char;
|
|
104
|
-
i++;
|
|
105
|
-
} else if (
|
|
106
|
-
char === '\\' &&
|
|
107
|
-
i + 1 < command.length &&
|
|
108
|
-
(command[i + 1] === quoteChar || command[i + 1] === '\\')
|
|
109
|
-
) {
|
|
110
|
-
// Handle escaped quotes and backslashes inside quotes
|
|
111
|
-
word += char;
|
|
112
|
-
i++;
|
|
113
|
-
if (i < command.length) {
|
|
114
|
-
word += command[i];
|
|
115
|
-
i++;
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
word += char;
|
|
119
|
-
i++;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
156
|
+
const operator = matchOperator(command, i);
|
|
157
|
+
if (operator) {
|
|
158
|
+
tokens.push({ type: operator.type, value: operator.value });
|
|
159
|
+
i += operator.length;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
123
162
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
// Parse word (respecting quotes)
|
|
164
|
+
const { word, endIndex } = parseWord(command, i);
|
|
165
|
+
i = endIndex;
|
|
166
|
+
|
|
167
|
+
if (word) {
|
|
168
|
+
tokens.push({ type: TokenType.WORD, value: word });
|
|
127
169
|
}
|
|
128
170
|
}
|
|
129
171
|
|
|
@@ -8,19 +8,27 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
10
10
|
|
|
11
|
+
// Helper to check if a listener is a command-stream SIGINT handler
|
|
12
|
+
function isCommandStreamListener(l) {
|
|
13
|
+
const str = l.toString();
|
|
14
|
+
return (
|
|
15
|
+
str.includes('findActiveRunners') ||
|
|
16
|
+
str.includes('forwardSigintToRunners') ||
|
|
17
|
+
str.includes('handleSigintExit') ||
|
|
18
|
+
str.includes('activeProcessRunners') ||
|
|
19
|
+
str.includes('ProcessRunner') ||
|
|
20
|
+
str.includes('activeChildren')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
// Helper to access internal state for testing
|
|
12
25
|
// This is a testing-only approach to verify cleanup
|
|
13
26
|
function getInternalState() {
|
|
14
27
|
// We'll use process listeners as a proxy for internal state
|
|
15
28
|
const sigintListeners = process.listeners('SIGINT');
|
|
16
|
-
const commandStreamListeners = sigintListeners.filter(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
str.includes('activeProcessRunners') ||
|
|
20
|
-
str.includes('ProcessRunner') ||
|
|
21
|
-
str.includes('activeChildren')
|
|
22
|
-
);
|
|
23
|
-
});
|
|
29
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
30
|
+
isCommandStreamListener
|
|
31
|
+
);
|
|
24
32
|
|
|
25
33
|
return {
|
|
26
34
|
sigintHandlerCount: commandStreamListeners.length,
|
|
@@ -50,14 +58,9 @@ describe('Resource Cleanup Internal Verification', () => {
|
|
|
50
58
|
|
|
51
59
|
// Force remove any command-stream SIGINT listeners
|
|
52
60
|
const sigintListeners = process.listeners('SIGINT');
|
|
53
|
-
const commandStreamListeners = sigintListeners.filter(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
str.includes('activeProcessRunners') ||
|
|
57
|
-
str.includes('ProcessRunner') ||
|
|
58
|
-
str.includes('activeChildren')
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
62
|
+
isCommandStreamListener
|
|
63
|
+
);
|
|
61
64
|
|
|
62
65
|
commandStreamListeners.forEach((listener) => {
|
|
63
66
|
process.removeListener('SIGINT', listener);
|
|
@@ -431,14 +434,9 @@ describe('Resource Cleanup Internal Verification', () => {
|
|
|
431
434
|
`Pipeline error test left behind ${state.sigintHandlerCount - initialState.sigintHandlerCount} handlers, forcing cleanup...`
|
|
432
435
|
);
|
|
433
436
|
const sigintListeners = process.listeners('SIGINT');
|
|
434
|
-
const commandStreamListeners = sigintListeners.filter(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
str.includes('activeProcessRunners') ||
|
|
438
|
-
str.includes('ProcessRunner') ||
|
|
439
|
-
str.includes('activeChildren')
|
|
440
|
-
);
|
|
441
|
-
});
|
|
437
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
438
|
+
isCommandStreamListener
|
|
439
|
+
);
|
|
442
440
|
|
|
443
441
|
commandStreamListeners.forEach((listener) => {
|
|
444
442
|
process.removeListener('SIGINT', listener);
|
|
@@ -82,6 +82,9 @@ describe.skipIf(isWindows)('SIGINT Handler Cleanup Tests', () => {
|
|
|
82
82
|
const ourListeners = process.listeners('SIGINT').filter((l) => {
|
|
83
83
|
const str = l.toString();
|
|
84
84
|
return (
|
|
85
|
+
str.includes('findActiveRunners') ||
|
|
86
|
+
str.includes('forwardSigintToRunners') ||
|
|
87
|
+
str.includes('handleSigintExit') ||
|
|
85
88
|
str.includes('activeProcessRunners') ||
|
|
86
89
|
str.includes('ProcessRunner') ||
|
|
87
90
|
str.includes('activeChildren')
|
package/package.json
CHANGED
package/rust/src/ansi.rs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//! ANSI control character utilities for command-stream
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles stripping and processing of ANSI escape codes
|
|
4
|
+
//! and control characters from text output.
|
|
5
|
+
|
|
6
|
+
/// ANSI control character utilities
|
|
7
|
+
pub struct AnsiUtils;
|
|
8
|
+
|
|
9
|
+
impl AnsiUtils {
|
|
10
|
+
/// Strip ANSI escape sequences from text
|
|
11
|
+
///
|
|
12
|
+
/// Removes color codes, cursor movement, and other ANSI escape sequences
|
|
13
|
+
/// while preserving the actual text content.
|
|
14
|
+
///
|
|
15
|
+
/// # Examples
|
|
16
|
+
///
|
|
17
|
+
/// ```
|
|
18
|
+
/// use command_stream::ansi::AnsiUtils;
|
|
19
|
+
///
|
|
20
|
+
/// let text = "\x1b[31mRed text\x1b[0m";
|
|
21
|
+
/// assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
|
|
22
|
+
/// ```
|
|
23
|
+
pub fn strip_ansi(text: &str) -> String {
|
|
24
|
+
let re = regex::Regex::new(r"\x1b\[[0-9;]*[mGKHFJ]").unwrap();
|
|
25
|
+
re.replace_all(text, "").to_string()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Strip control characters from text, preserving newlines, carriage returns, and tabs
|
|
29
|
+
///
|
|
30
|
+
/// Removes control characters (ASCII 0x00-0x1F and 0x7F) except:
|
|
31
|
+
/// - Newlines (\n = 0x0A)
|
|
32
|
+
/// - Carriage returns (\r = 0x0D)
|
|
33
|
+
/// - Tabs (\t = 0x09)
|
|
34
|
+
///
|
|
35
|
+
/// # Examples
|
|
36
|
+
///
|
|
37
|
+
/// ```
|
|
38
|
+
/// use command_stream::ansi::AnsiUtils;
|
|
39
|
+
///
|
|
40
|
+
/// let text = "Hello\x00World\nNew line\tTab";
|
|
41
|
+
/// assert_eq!(AnsiUtils::strip_control_chars(text), "HelloWorld\nNew line\tTab");
|
|
42
|
+
/// ```
|
|
43
|
+
pub fn strip_control_chars(text: &str) -> String {
|
|
44
|
+
text.chars()
|
|
45
|
+
.filter(|c| {
|
|
46
|
+
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
|
|
47
|
+
!matches!(*c as u32,
|
|
48
|
+
0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
.collect()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Strip both ANSI sequences and control characters
|
|
55
|
+
///
|
|
56
|
+
/// Combines `strip_ansi` and `strip_control_chars` for complete text cleaning.
|
|
57
|
+
pub fn strip_all(text: &str) -> String {
|
|
58
|
+
Self::strip_control_chars(&Self::strip_ansi(text))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Clean data for processing (strips ANSI and control chars)
|
|
62
|
+
///
|
|
63
|
+
/// Alias for `strip_all` - provides semantic clarity when processing
|
|
64
|
+
/// data that needs to be cleaned for further processing.
|
|
65
|
+
pub fn clean_for_processing(data: &str) -> String {
|
|
66
|
+
Self::strip_all(data)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Configuration for ANSI handling
|
|
71
|
+
///
|
|
72
|
+
/// Controls how ANSI escape codes and control characters are processed
|
|
73
|
+
/// in command output.
|
|
74
|
+
#[derive(Debug, Clone)]
|
|
75
|
+
pub struct AnsiConfig {
|
|
76
|
+
/// Whether to preserve ANSI escape sequences in output
|
|
77
|
+
pub preserve_ansi: bool,
|
|
78
|
+
/// Whether to preserve control characters in output
|
|
79
|
+
pub preserve_control_chars: bool,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl Default for AnsiConfig {
|
|
83
|
+
fn default() -> Self {
|
|
84
|
+
AnsiConfig {
|
|
85
|
+
preserve_ansi: true,
|
|
86
|
+
preserve_control_chars: true,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl AnsiConfig {
|
|
92
|
+
/// Create a new AnsiConfig that preserves everything (default)
|
|
93
|
+
pub fn new() -> Self {
|
|
94
|
+
Self::default()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Create a config that strips all ANSI and control characters
|
|
98
|
+
pub fn strip_all() -> Self {
|
|
99
|
+
AnsiConfig {
|
|
100
|
+
preserve_ansi: false,
|
|
101
|
+
preserve_control_chars: false,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Process output according to config settings
|
|
106
|
+
///
|
|
107
|
+
/// Applies the configured stripping rules to the input data.
|
|
108
|
+
pub fn process_output(&self, data: &str) -> String {
|
|
109
|
+
if !self.preserve_ansi && !self.preserve_control_chars {
|
|
110
|
+
AnsiUtils::clean_for_processing(data)
|
|
111
|
+
} else if !self.preserve_ansi {
|
|
112
|
+
AnsiUtils::strip_ansi(data)
|
|
113
|
+
} else if !self.preserve_control_chars {
|
|
114
|
+
AnsiUtils::strip_control_chars(data)
|
|
115
|
+
} else {
|
|
116
|
+
data.to_string()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[cfg(test)]
|
|
122
|
+
mod tests {
|
|
123
|
+
use super::*;
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn test_strip_ansi() {
|
|
127
|
+
let text = "\x1b[31mRed text\x1b[0m";
|
|
128
|
+
assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn test_strip_ansi_multiple_codes() {
|
|
133
|
+
let text = "\x1b[1m\x1b[32mBold Green\x1b[0m Normal";
|
|
134
|
+
assert_eq!(AnsiUtils::strip_ansi(text), "Bold Green Normal");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn test_strip_control_chars() {
|
|
139
|
+
let text = "Hello\x00World\nNew line\tTab";
|
|
140
|
+
assert_eq!(
|
|
141
|
+
AnsiUtils::strip_control_chars(text),
|
|
142
|
+
"HelloWorld\nNew line\tTab"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn test_strip_control_chars_preserves_whitespace() {
|
|
148
|
+
let text = "Line1\nLine2\r\nLine3\tTabbed";
|
|
149
|
+
assert_eq!(
|
|
150
|
+
AnsiUtils::strip_control_chars(text),
|
|
151
|
+
"Line1\nLine2\r\nLine3\tTabbed"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_strip_all() {
|
|
157
|
+
let text = "\x1b[31mRed\x00text\x1b[0m";
|
|
158
|
+
assert_eq!(AnsiUtils::strip_all(text), "Redtext");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn test_ansi_config_default() {
|
|
163
|
+
let config = AnsiConfig::default();
|
|
164
|
+
let text = "\x1b[31mRed\x00text\x1b[0m";
|
|
165
|
+
assert_eq!(config.process_output(text), text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn test_ansi_config_strip_all() {
|
|
170
|
+
let config = AnsiConfig::strip_all();
|
|
171
|
+
let text = "\x1b[31mRed\x00text\x1b[0m";
|
|
172
|
+
assert_eq!(config.process_output(text), "Redtext");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn test_ansi_config_strip_ansi_only() {
|
|
177
|
+
let config = AnsiConfig {
|
|
178
|
+
preserve_ansi: false,
|
|
179
|
+
preserve_control_chars: true,
|
|
180
|
+
};
|
|
181
|
+
let text = "\x1b[31mRed text\x1b[0m";
|
|
182
|
+
assert_eq!(config.process_output(text), "Red text");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn test_ansi_config_strip_control_only() {
|
|
187
|
+
let config = AnsiConfig {
|
|
188
|
+
preserve_ansi: true,
|
|
189
|
+
preserve_control_chars: false,
|
|
190
|
+
};
|
|
191
|
+
let text = "Hello\x00World";
|
|
192
|
+
assert_eq!(config.process_output(text), "HelloWorld");
|
|
193
|
+
}
|
|
194
|
+
}
|