add support to ignore ansi sequences when formatting usage display. Fixes #879#942
add support to ignore ansi sequences when formatting usage display. Fixes #879#942timmaffett wants to merge 20 commits intodart-lang:mainfrom
Conversation
| /// Matches the Control Sequence Introducer (CSI) ANSI escape sequences. | ||
| /// | ||
| /// Anatomy: | ||
| /// \x1b : The literal ESC character (ASCII 27). | ||
| /// \[ : The literal '[' character (together with ESC, this forms the CSI). | ||
| /// [0-9;?]* : Zero or more parameter bytes: | ||
| /// - 0-9 : Numeric parameters (e.g., color codes). | ||
| /// - ; : Parameter separators. | ||
| /// - ? : Private mode indicators (e.g., cursor toggles). | ||
| /// [a-zA-Z] : The 'Final Byte' that determines the command (e.g., 'm' for color). | ||
| static final RegExp _ansiRegex = RegExp(r'\x1b\[[0-9;?]*[a-zA-Z]'); |
There was a problem hiding this comment.
This covers the full set I've seen used in Dart, but I think we may as well expand the regex to include the other parameter bytes and intermediate bytes allowed.
| /// Matches the Control Sequence Introducer (CSI) ANSI escape sequences. | |
| /// | |
| /// Anatomy: | |
| /// \x1b : The literal ESC character (ASCII 27). | |
| /// \[ : The literal '[' character (together with ESC, this forms the CSI). | |
| /// [0-9;?]* : Zero or more parameter bytes: | |
| /// - 0-9 : Numeric parameters (e.g., color codes). | |
| /// - ; : Parameter separators. | |
| /// - ? : Private mode indicators (e.g., cursor toggles). | |
| /// [a-zA-Z] : The 'Final Byte' that determines the command (e.g., 'm' for color). | |
| static final RegExp _ansiRegex = RegExp(r'\x1b\[[0-9;?]*[a-zA-Z]'); | |
| /// Matches the Control Sequence Introducer (CSI) ANSI escape sequences. | |
| /// | |
| /// Anatomy based on ECMA-48: | |
| /// \x1b : The literal ESC character (ASCII 27). | |
| /// \[ : The literal '[' character (together with ESC, this forms the CSI). | |
| /// [\x30-\x3f]* : Parameter Bytes (0-9:;<=>?). | |
| /// [\x20-\x2f]* : Intermediate Bytes ( !"#$%&'()*+,-./). | |
| /// [\x40-\x7e] : Final Byte (@A-Z[\]^_`a-z{|}~). | |
| static final RegExp _ansiRegex = RegExp(r'\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]'); |
There was a problem hiding this comment.
OK, I have change it to now use your more complete regex. I also expanded the tests to exercise a wider range of ECMA-48 ANSI sequences.
|
Thanks for the quick feedback @natebosch In my first attempt at the simpler regex I was just thinking about catching ANSI text styling codes that would typically be used in a usage message, but this one is not that much more complex and it will now catch everything. |
| String stripAnsi() => replaceAll(_ansiRegex, ''); | ||
|
|
||
| /// Returns `true` if the string contains any ANSI escape sequences. | ||
| bool hasAnsi() { |
There was a problem hiding this comment.
Should be getter.
Document as
/// Whether this string contains any ANSI escape sequences.|
|
||
| group('ANSI RegEx Systematic Tests', () { | ||
|
|
||
| test('Identifies standard SGR (Select Graphic Rendition) codes', () { |
There was a problem hiding this comment.
Consider some more negative edge-case test (if they aren't already here and I just missed them):
- Character between ESC and
[:'\x1b${char}[m'wherecharis some char other[or ESC, including fxr'\'. (Or where it is ESC, just to be sure it doesn't count the first one.) - Non-valid character instead of terminator:
'\x1b[${char}wherecharis0x7for a control character like 0x00-0x1F. Or a non-ASCII character (fx a valid character + 0x80, +0x100, +0xD800, and +0x10000). - Or in general, try off-by-0x80/0x100/0x1000/0xd800/0x10000 characters in the parameters and intermediate bytes too. Or even ESC and
[:${String.fromCharCode(0x1b + offset)}[0m,\x1b${String.fromCharCode(0x5b + offset)}0m,\x1b[${String.fromCharCode(0x30 + offset)}m,\x1b[0${String.fromCharCode(0x20 + offset)}m,\x1b[0${String.fromCharCode(0x6d + offset)}, withoffsetbeing one on of those numbers.
and check that nothing is matched.
(At least this is UTF-16 strings, not UTF-8, no overlong encodings to worry about 😅 .)
There was a problem hiding this comment.
OK, i added test for all of these edge cases.
| } | ||
|
|
||
| /// Pads [source] to [length] by adding spaces at the end. | ||
| String padRight(String source, int length) => |
There was a problem hiding this comment.
(Could be extension member too, but would be named padRightIgnoreAnsi.)
There was a problem hiding this comment.
padRightIgnoreAscii extension now
# Conflicts: # pkgs/args/CHANGELOG.md
|
OK @irhn I have address all of your comments - Thank you for taking the time for a thorough and the great feedback! I also included a modified example of the ArgParser example that uses extensive ANSI sequences just so I could verify it in action. I am happy to remove this if needed. |
Avoid third_party dependencies from core Dart team repos.
Package publishing
Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. |
PR Health
Coverage
|
| File | Coverage |
|---|---|
| pkgs/args/example/arg_parser/ansi_example.dart | 💔 Not covered |
| pkgs/args/lib/command_runner.dart | 💔 Not covered |
| pkgs/args/lib/src/usage.dart | 💔 Not covered |
| pkgs/args/lib/src/utils.dart | 💔 Not covered |
This check for test coverage is informational (issues shown here will not fail the PR).
This check can be disabled by tagging the PR with skip-coverage-check.
Breaking changes ✔️
| Package | Change | Current Version | New Version | Needed Version | Looking good? |
|---|---|---|---|---|---|
| args | Breaking | 2.7.0 | 2.8.0-wip | 2.8.0-wip | ✔️ |
This check can be disabled by tagging the PR with skip-breaking-check.
Unused Dependencies ⚠️
| Package | Status |
|---|---|
| args | ❗ Show IssuesThese packages are used outside lib/ but are not dev_dependencies: |
For details on how to fix these, see dependency_validator.
This check can be disabled by tagging the PR with skip-unused-dependencies-check.
Changelog Entry ✔️
| Package | Changed Files |
|---|
Changes to files need to be accounted for in their respective changelogs.
This check can be disabled by tagging the PR with skip-changelog-check.
License Headers ⚠️
""
| Files |
|---|
| pkgs/args/lib/src/usage.dart |
| pkgs/args/lib/src/utils.dart |
| pkgs/args/test/utils_test.dart |
All source files should start with a license header.
Unrelated files missing license headers
| Files |
|---|
| pkgs/args/example/command_runner/draw.dart |
| pkgs/args/lib/args.dart |
| pkgs/args/lib/src/allow_anything_parser.dart |
| pkgs/args/lib/src/arg_parser.dart |
| pkgs/args/lib/src/arg_parser_exception.dart |
| pkgs/args/lib/src/arg_results.dart |
| pkgs/args/lib/src/help_command.dart |
| pkgs/args/lib/src/option.dart |
| pkgs/args/lib/src/parser.dart |
| pkgs/args/lib/src/usage_exception.dart |
| pkgs/args/test/allow_anything_test.dart |
| pkgs/args/test/args_test.dart |
| pkgs/args/test/command_parse_test.dart |
| pkgs/args/test/command_runner_test.dart |
| pkgs/args/test/command_test.dart |
| pkgs/args/test/parse_performance_test.dart |
| pkgs/args/test/parse_test.dart |
| pkgs/args/test/test_utils.dart |
| pkgs/args/test/trailing_options_test.dart |
| pkgs/args/test/usage_test.dart |
| pkgs/async/lib/async.dart |
| pkgs/async/lib/src/async_cache.dart |
| pkgs/async/lib/src/async_memoizer.dart |
| pkgs/async/lib/src/byte_collector.dart |
| pkgs/async/lib/src/cancelable_operation.dart |
| pkgs/async/lib/src/chunked_stream_reader.dart |
| pkgs/async/lib/src/delegate/event_sink.dart |
| pkgs/async/lib/src/delegate/future.dart |
| pkgs/async/lib/src/delegate/sink.dart |
| pkgs/async/lib/src/delegate/stream.dart |
| pkgs/async/lib/src/delegate/stream_consumer.dart |
| pkgs/async/lib/src/delegate/stream_sink.dart |
| pkgs/async/lib/src/delegate/stream_subscription.dart |
| pkgs/async/lib/src/future_group.dart |
| pkgs/async/lib/src/lazy_stream.dart |
| pkgs/async/lib/src/null_stream_sink.dart |
| pkgs/async/lib/src/restartable_timer.dart |
| pkgs/async/lib/src/result/capture_sink.dart |
| pkgs/async/lib/src/result/capture_transformer.dart |
| pkgs/async/lib/src/result/error.dart |
| pkgs/async/lib/src/result/future.dart |
| pkgs/async/lib/src/result/release_sink.dart |
| pkgs/async/lib/src/result/release_transformer.dart |
| pkgs/async/lib/src/result/result.dart |
| pkgs/async/lib/src/result/value.dart |
| pkgs/async/lib/src/single_subscription_transformer.dart |
| pkgs/async/lib/src/sink_base.dart |
| pkgs/async/lib/src/stream_closer.dart |
| pkgs/async/lib/src/stream_completer.dart |
| pkgs/async/lib/src/stream_extensions.dart |
| pkgs/async/lib/src/stream_group.dart |
| pkgs/async/lib/src/stream_queue.dart |
| pkgs/async/lib/src/stream_sink_completer.dart |
| pkgs/async/lib/src/stream_sink_extensions.dart |
| pkgs/async/lib/src/stream_sink_transformer.dart |
| pkgs/async/lib/src/stream_sink_transformer/handler_transformer.dart |
| pkgs/async/lib/src/stream_sink_transformer/reject_errors.dart |
| pkgs/async/lib/src/stream_sink_transformer/stream_transformer_wrapper.dart |
| pkgs/async/lib/src/stream_sink_transformer/typed.dart |
| pkgs/async/lib/src/stream_splitter.dart |
| pkgs/async/lib/src/stream_subscription_transformer.dart |
| pkgs/async/lib/src/stream_zip.dart |
| pkgs/async/lib/src/subscription_stream.dart |
| pkgs/async/lib/src/typed/stream_subscription.dart |
| pkgs/async/lib/src/typed_stream_transformer.dart |
| pkgs/async/test/async_cache_test.dart |
| pkgs/async/test/async_memoizer_test.dart |
| pkgs/async/test/byte_collection_test.dart |
| pkgs/async/test/cancelable_operation_test.dart |
| pkgs/async/test/chunked_stream_reader.dart |
| pkgs/async/test/future_group_test.dart |
| pkgs/async/test/io_sink_impl.dart |
| pkgs/async/test/lazy_stream_test.dart |
| pkgs/async/test/null_stream_sink_test.dart |
| pkgs/async/test/reject_errors_test.dart |
| pkgs/async/test/restartable_timer_test.dart |
| pkgs/async/test/result/result_captureAll_test.dart |
| pkgs/async/test/result/result_flattenAll_test.dart |
| pkgs/async/test/result/result_future_test.dart |
| pkgs/async/test/result/result_test.dart |
| pkgs/async/test/single_subscription_transformer_test.dart |
| pkgs/async/test/sink_base_test.dart |
| pkgs/async/test/stream_closer_test.dart |
| pkgs/async/test/stream_completer_test.dart |
| pkgs/async/test/stream_extensions_test.dart |
| pkgs/async/test/stream_group_test.dart |
| pkgs/async/test/stream_queue_test.dart |
| pkgs/async/test/stream_sink_completer_test.dart |
| pkgs/async/test/stream_sink_transformer_test.dart |
| pkgs/async/test/stream_splitter_test.dart |
| pkgs/async/test/stream_zip_test.dart |
| pkgs/async/test/stream_zip_zone_test.dart |
| pkgs/async/test/subscription_stream_test.dart |
| pkgs/async/test/subscription_transformer_test.dart |
| pkgs/async/test/typed_wrapper/stream_subscription_test.dart |
| pkgs/async/test/utils.dart |
| pkgs/characters/benchmark/benchmark.dart |
| pkgs/characters/example/main.dart |
| pkgs/characters/lib/characters.dart |
| pkgs/characters/lib/src/characters.dart |
| pkgs/characters/lib/src/characters_impl.dart |
| pkgs/characters/lib/src/extensions.dart |
| pkgs/characters/test/characters_test.dart |
| pkgs/characters/test/src/equiv.dart |
| pkgs/characters/test/src/text_samples.dart |
| pkgs/characters/test/src/unicode_tests.dart |
| pkgs/characters/test/src/various_tests.dart |
| pkgs/characters/tool/benchmark.dart |
| pkgs/characters/tool/bin/generate_tables.dart |
| pkgs/characters/tool/bin/generate_tests.dart |
| pkgs/characters/tool/generate.dart |
| pkgs/characters/tool/src/atsp.dart |
| pkgs/characters/tool/src/data_files.dart |
| pkgs/characters/tool/src/debug_names.dart |
| pkgs/characters/tool/src/graph.dart |
| pkgs/characters/tool/src/indirect_table.dart |
| pkgs/characters/tool/src/list_overlap.dart |
| pkgs/characters/tool/src/shared.dart |
| pkgs/characters/tool/src/string_literal_writer.dart |
| pkgs/characters/tool/src/table_builder.dart |
| pkgs/collection/benchmark/benchmark_utils.dart |
| pkgs/collection/benchmark/deep_collection_equality.dart |
| pkgs/collection/benchmark/legacy_quicksort.dart |
| pkgs/collection/benchmark/sort_benchmark.dart |
| pkgs/collection/lib/algorithms.dart |
| pkgs/collection/lib/collection.dart |
| pkgs/collection/lib/equality.dart |
| pkgs/collection/lib/iterable_zip.dart |
| pkgs/collection/lib/priority_queue.dart |
| pkgs/collection/lib/src/algorithms.dart |
| pkgs/collection/lib/src/boollist.dart |
| pkgs/collection/lib/src/canonicalized_map.dart |
| pkgs/collection/lib/src/combined_wrappers/combined_iterable.dart |
| pkgs/collection/lib/src/combined_wrappers/combined_iterator.dart |
| pkgs/collection/lib/src/combined_wrappers/combined_list.dart |
| pkgs/collection/lib/src/combined_wrappers/combined_map.dart |
| pkgs/collection/lib/src/comparators.dart |
| pkgs/collection/lib/src/empty_unmodifiable_set.dart |
| pkgs/collection/lib/src/equality.dart |
| pkgs/collection/lib/src/equality_map.dart |
| pkgs/collection/lib/src/equality_set.dart |
| pkgs/collection/lib/src/functions.dart |
| pkgs/collection/lib/src/iterable_extensions.dart |
| pkgs/collection/lib/src/iterable_zip.dart |
| pkgs/collection/lib/src/list_extensions.dart |
| pkgs/collection/lib/src/priority_queue.dart |
| pkgs/collection/lib/src/queue_list.dart |
| pkgs/collection/lib/src/union_set.dart |
| pkgs/collection/lib/src/union_set_controller.dart |
| pkgs/collection/lib/src/unmodifiable_wrappers.dart |
| pkgs/collection/lib/src/utils.dart |
| pkgs/collection/lib/src/wrappers.dart |
| pkgs/collection/lib/wrappers.dart |
| pkgs/collection/test/algorithms_test.dart |
| pkgs/collection/test/boollist_test.dart |
| pkgs/collection/test/canonicalized_map_test.dart |
| pkgs/collection/test/combined_wrapper/iterable_test.dart |
| pkgs/collection/test/combined_wrapper/list_test.dart |
| pkgs/collection/test/combined_wrapper/map_test.dart |
| pkgs/collection/test/comparators_test.dart |
| pkgs/collection/test/equality_map_test.dart |
| pkgs/collection/test/equality_set_test.dart |
| pkgs/collection/test/equality_test.dart |
| pkgs/collection/test/extensions_test.dart |
| pkgs/collection/test/functions_test.dart |
| pkgs/collection/test/ignore_ascii_case_test.dart |
| pkgs/collection/test/iterable_zip_test.dart |
| pkgs/collection/test/priority_queue_test.dart |
| pkgs/collection/test/queue_list_test.dart |
| pkgs/collection/test/separate_extensions_test.dart |
| pkgs/collection/test/union_set_controller_test.dart |
| pkgs/collection/test/union_set_test.dart |
| pkgs/collection/test/unmodifiable_collection_test.dart |
| pkgs/collection/test/wrapper_test.dart |
| pkgs/convert/benchmark/fixed_datetime_formatter_benchmark.dart |
| pkgs/convert/example/example.dart |
| pkgs/convert/lib/convert.dart |
| pkgs/convert/lib/src/accumulator_sink.dart |
| pkgs/convert/lib/src/byte_accumulator_sink.dart |
| pkgs/convert/lib/src/charcodes.dart |
| pkgs/convert/lib/src/fixed_datetime_formatter.dart |
| pkgs/convert/lib/src/hex.dart |
| pkgs/convert/lib/src/hex/decoder.dart |
| pkgs/convert/lib/src/identity_codec.dart |
| pkgs/convert/lib/src/percent.dart |
| pkgs/convert/lib/src/percent/decoder.dart |
| pkgs/convert/lib/src/string_accumulator_sink.dart |
| pkgs/convert/lib/src/utils.dart |
| pkgs/convert/test/accumulator_sink_test.dart |
| pkgs/convert/test/byte_accumulator_sink_test.dart |
| pkgs/convert/test/codepage_test.dart |
| pkgs/convert/test/fixed_datetime_formatter_test.dart |
| pkgs/convert/test/hex_test.dart |
| pkgs/convert/test/identity_codec_test.dart |
| pkgs/convert/test/percent_test.dart |
| pkgs/convert/test/string_accumulator_sink_test.dart |
| pkgs/crypto/benchmark/benchmark.dart |
| pkgs/crypto/example/example.dart |
| pkgs/crypto/lib/crypto.dart |
| pkgs/crypto/lib/src/digest.dart |
| pkgs/crypto/lib/src/digest_sink.dart |
| pkgs/crypto/lib/src/hash.dart |
| pkgs/crypto/lib/src/hash_sink.dart |
| pkgs/crypto/lib/src/hmac.dart |
| pkgs/crypto/lib/src/md5.dart |
| pkgs/crypto/lib/src/sha1.dart |
| pkgs/crypto/lib/src/sha256.dart |
| pkgs/crypto/lib/src/sha512.dart |
| pkgs/crypto/lib/src/sha512_fastsinks.dart |
| pkgs/crypto/lib/src/sha512_slowsinks.dart |
| pkgs/crypto/lib/src/utils.dart |
| pkgs/crypto/test/hmac_md5_test.dart |
| pkgs/crypto/test/hmac_sha1_test.dart |
| pkgs/crypto/test/hmac_sha256_test.dart |
| pkgs/crypto/test/hmac_sha2_test.dart |
| pkgs/crypto/test/sha1_test.dart |
| pkgs/crypto/test/sha256_test.dart |
| pkgs/crypto/test/sha512_test.dart |
| pkgs/crypto/test/sha_monte_test.dart |
| pkgs/crypto/test/utils.dart |
| pkgs/crypto/tool/md5sum.dart |
| pkgs/fixnum/lib/fixnum.dart |
| pkgs/fixnum/lib/src/int32.dart |
| pkgs/fixnum/lib/src/int64.dart |
| pkgs/fixnum/lib/src/int64_emulated.dart |
| pkgs/fixnum/lib/src/int64_native.dart |
| pkgs/fixnum/lib/src/intx.dart |
| pkgs/fixnum/lib/src/utilities.dart |
| pkgs/fixnum/test/all_tests.dart |
| pkgs/fixnum/test/int32_test.dart |
| pkgs/fixnum/test/int64_test.dart |
| pkgs/fixnum/test/int_64_vm_test.dart |
| pkgs/lints/tool/gen_docs.dart |
| pkgs/lints/tool/validate_lib.dart |
| pkgs/logging/example/main.dart |
| pkgs/logging/lib/logging.dart |
| pkgs/logging/lib/src/level.dart |
| pkgs/logging/lib/src/log_record.dart |
| pkgs/logging/lib/src/logger.dart |
| pkgs/logging/test/logging_test.dart |
| pkgs/os_detect/bin/os_detect.dart |
| pkgs/os_detect/example/example.dart |
| pkgs/os_detect/example/tree_shaking.dart |
| pkgs/os_detect/lib/os_detect.dart |
| pkgs/os_detect/lib/override.dart |
| pkgs/os_detect/lib/src/os_kind.dart |
| pkgs/os_detect/lib/src/os_override.dart |
| pkgs/os_detect/lib/src/osid_html.dart |
| pkgs/os_detect/lib/src/osid_io.dart |
| pkgs/os_detect/lib/src/osid_unknown.dart |
| pkgs/os_detect/test/osid_test.dart |
| pkgs/path/benchmark/benchmark.dart |
| pkgs/path/example/example.dart |
| pkgs/path/lib/path.dart |
| pkgs/path/lib/src/characters.dart |
| pkgs/path/lib/src/context.dart |
| pkgs/path/lib/src/internal_style.dart |
| pkgs/path/lib/src/path_exception.dart |
| pkgs/path/lib/src/path_map.dart |
| pkgs/path/lib/src/path_set.dart |
| pkgs/path/lib/src/style.dart |
| pkgs/path/lib/src/style/posix.dart |
| pkgs/path/lib/src/style/url.dart |
| pkgs/path/lib/src/style/windows.dart |
| pkgs/path/lib/src/utils.dart |
| pkgs/path/test/browser_test.dart |
| pkgs/path/test/io_test.dart |
| pkgs/path/test/path_map_test.dart |
| pkgs/path/test/path_set_test.dart |
| pkgs/path/test/path_test.dart |
| pkgs/path/test/posix_test.dart |
| pkgs/path/test/relative_test.dart |
| pkgs/path/test/url_test.dart |
| pkgs/path/test/utils.dart |
| pkgs/path/test/windows_test.dart |
| pkgs/typed_data/lib/src/typed_buffer.dart |
| pkgs/typed_data/lib/src/typed_queue.dart |
| pkgs/typed_data/lib/typed_buffers.dart |
| pkgs/typed_data/lib/typed_data.dart |
| pkgs/typed_data/test/queue_test.dart |
| pkgs/typed_data/test/typed_buffers_test.dart |
| pkgs/typed_data/test/typed_buffers_vm_test.dart |
This check can be disabled by tagging the PR with skip-license-check.
API leaks ✔️
The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.
| Package | Leaked API symbol | Leaking sources |
|---|
This check can be disabled by tagging the PR with skip-leaking-check.
|
We avoid dependencies to third party packages from these repos so I've gone ahead and rewritten the example to use a different ansi library - it's not quite as full featured in terms of colors covered, but it shows the intent just as well. |
| }); | ||
|
|
||
| group('lengthWithoutAnsi is correct with no ANSI sequences', () { | ||
| test('lengthWithoutAnsi returns correct length on lines without ansi', () { |
There was a problem hiding this comment.
Some of these tests appear to be duplicates. Were they copy/pasted?
There was a problem hiding this comment.
It is quite possible while I was implementing @lrhn 's suggestions i inadvertently pasted or created a duplicate. Thanks for cleaning it up.
natebosch
left a comment
There was a problem hiding this comment.
Took a closer look at the tests and I think we may be over-testing.
| RegExp(r'\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]'); | ||
|
|
||
| /// Combined length of all ANSI escape sequences in the string. | ||
| int get ansiLength { |
There was a problem hiding this comment.
@lrhn - I'm inclined to make more of these implementation details private and remove the tests of the individual APIs, leaving only the tests for lengthWithoutAnsi and padWithoutAnsi. Do you have any concerns?
This is a RE-push of the #880 PR - the bot closed it and now the code does not update/so I made this PR so that the new changes can be seen. If the #880 PR got re-opened it would probably update and this could be closed...?
---From ORIGINAL #880 PR ----------------
This PR adds logic to exclude hidden ANSI escape sequences when formatting the Usage display.
The ANSI escape sequences are excluded when calculating the lengths of help strings used in the Usage display by using a string extension lengthWithoutAnsi which removes any hidden ANSI escape sequences present before calculating the string length.
It includes tests of the lengthWithoutAnsi getter to ensure that it returns correct length values when no ANSI escape sequences are present as well as when a variety of ANSI escape sequences are present.
(The regex that i am using here is well exercised, as it is the same reg ex that I use within VS Code when parsing and formatting ANSI sequences within the debug console)