txt-regex — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited txt-regex (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Authored against iOS 26.x / Swift 6.x / Xcode 26.x.
The regex landscape has two engines: Swift Regex (iOS 16+, compile-time validated, type-safe captures, integrated with Foundation parsers) and NSRegularExpression (every OS, ICU-backed, NSRange-native). The right pick is rarely about performance — it is usually about deployment target and whether the consumer wants Range<String.Index> or NSRange. The patterns below are starting points; before quoting any specific API signature, fetch the current Apple docs via Sosumi (sosumi.ai/documentation/swift/regex) and verify against the actual code, especially around bridging — most "match in the wrong place" bugs are an NSRange / String.Index mismatch, not a pattern bug.
A pattern that works in storage is not the same as a pattern that works in the displayed string after Bidi reordering or normalization. When the result feeds an attribute application against an NSTextStorage, the offsets must round-trip cleanly between the engine's representation and NSRange.
Three creation paths, with different tradeoffs.
A regex literal (/pattern/) is parsed at compile time. Syntax errors are build failures; capture types are inferred and strongly typed. The result is a Regex<(Substring, Substring, ...)> whose tuple shape matches the captures. This is the right default for static patterns:
let email = /(?<user>\w+)@(?<domain>\w+\.\w+)/
if let m = "[email protected]".firstMatch(of: email) {
let user = m.user // Substring
let domain = m.domain // Substring
}A string-constructed Regex(patternString) runs at runtime. It throws on invalid patterns and produces AnyRegexOutput, which loses the typed-tuple shape. This is the path for user-supplied patterns:
let r = try Regex(userPattern)The RegexBuilder DSL builds a regex from typed components. It is verbose but self-documenting and modular — the right answer for a pattern complex enough that the literal becomes unreadable, or one that mixes literal text with Foundation parsers:
import RegexBuilder
let parser = Regex {
"Date: "
Capture { .date(.numeric, locale: .current, timeZone: .current) }
}
if let m = "Date: 03/15/2025".firstMatch(of: parser) {
let date: Date = m.1 // Actual Date, not Substring
}Foundation parsers (.date, .localizedCurrency, .localizedInteger) are the largest concrete win over NSRegularExpression: a single pass produces typed values, not strings that need a second parse step.
Swift Regex is Unicode-correct by default — matches operate on extended grapheme clusters, with canonical equivalence applied. NSRegularExpression operates on UTF-16 code units, which produces the wrong answer for emoji and combining-mark text unless the pattern is hand-tuned.
The string methods that take a regex — contains, firstMatch(of:), matches(of:), ranges(of:), replacing(_:with:), split(separator:), trimmingPrefix(_:) — all produce Range<String.Index>, not NSRange. Anything feeding NSAttributedString needs a bridge.
Available on every OS version, ICU-based, NSRange-native. The construction is throwing because a malformed pattern cannot be detected at compile time:
let r = try NSRegularExpression(pattern: "\\b(TODO|FIXME|HACK)\\b")enumerateMatches(in:options:range:using:) is the high-throughput path; firstMatch(in:options:range:) and matches(in:options:range:) are alternatives for one-shot or eager use. Each match is an NSTextCheckingResult whose range is the full match and whose range(at:) returns capture group ranges.
The natural pairing with TextKit is direct — no bridging:
let storage = textStorage
let r = try NSRegularExpression(pattern: "\\b(TODO|FIXME|HACK)\\b")
let full = NSRange(location: 0, length: storage.length)
r.enumerateMatches(in: storage.string, range: full) { match, _, _ in
guard let range = match?.range else { return }
storage.addAttribute(.foregroundColor, value: UIColor.orange, range: range)
}Two cautions. First, NSRegularExpression compilation is non-trivial; building the same regex inside a hot loop is wasteful. Hoist the construction outside any per-keystroke or per-frame work. Second, options like .anchorsMatchLines change anchor behavior — ^ and $ match start/end of document by default, not start/end of line. Per-line patterns need the option.
NSRange(range, in: string) converts a Range<String.Index> into UTF-16 offsets suitable for NSAttributedString and TextKit. The conversion is O(1) for contiguous strings and inexpensive in practice; the cost is one line of bridging at every call site:
let text = textStorage.string
if let m = text.firstMatch(of: /TODO:\s*(.+)/) {
let fullRange = NSRange(m.range, in: text)
let captureRange = NSRange(m.1.startIndex..<m.1.endIndex, in: text)
textStorage.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
textStorage.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 14),
range: captureRange)
}
for m in text.matches(of: /\b\w+\b/) {
let r = NSRange(m.range, in: text)
// r is now usable with TextKit
}The bridging is the cost of using Swift Regex against an NSAttributedString. For a syntax highlighter that runs on every keystroke against a long document, the per-call overhead is usually invisible relative to layout costs.
The reverse direction (Range(_:in:)) converts NSRange back to Range<String.Index>. It returns optional because not every NSRange describes a valid extended-grapheme-cluster boundary in the string.
Pure throughput is rarely the deciding factor. ICU is mature; the Swift Regex engine has caught up on simple patterns and is improving on complex ones. The differences that matter:
For complex backtracking-heavy patterns, Swift Regex offers Local { } (atomic groups) and .repetitionBehavior(.reluctant) to bound backtracking explicitly. NSRegularExpression can backtrack catastrophically on adversarial inputs unless the pattern is hand-tuned for it.
For one-shot use, the difference is negligible. For tight loops, hoist the regex out of the loop in either engine.
For a parser that needs to extract typed values (dates, currency), Swift Regex's Foundation parsers do it in one pass. NSRegularExpression captures the substring; a second parse step turns it into a typed value.
The deciding question is usually deployment target plus output type:
| Constraint | Pick |
|---|---|
| Must support pre-iOS 16 | NSRegularExpression |
| TextKit / NSAttributedString consumer, want zero bridging | NSRegularExpression |
| Static pattern in new code | Swift Regex literal |
| Captures need to be typed values, not Substrings | Swift Regex with Foundation parsers |
| User-supplied pattern | Either; both support runtime construction |
| Pattern complex enough that literal is illegible | RegexBuilder DSL |
The two engines diverge mostly in ergonomics. Both work; pick by deployment target and by whether the surrounding code already speaks NSRange or Range<String.Index>.
NSRegularExpression is the lighter touch when the consumer is NSTextStorage:
func highlight(in range: NSRange, storage: NSTextStorage) {
let text = storage.string
let kw = try! NSRegularExpression(
pattern: "\\b(func|var|let|class|struct|enum|if|else|for|while|return)\\b")
kw.enumerateMatches(in: text, range: range) { m, _, _ in
guard let r = m?.range else { return }
storage.addAttribute(.foregroundColor, value: UIColor.systemPink, range: r)
}
}Swift Regex with bridging is the lighter touch when the surrounding code is Swift-native and only the application step touches NSTextStorage:
func highlight(in nsRange: NSRange, storage: NSTextStorage) {
let text = storage.string
guard let swiftRange = Range(nsRange, in: text) else { return }
let sub = text[swiftRange]
for m in sub.matches(of: /\b(func|var|let|class|struct|enum|if|else|for|while|return)\b/) {
let r = NSRange(m.range, in: text)
storage.addAttribute(.foregroundColor, value: UIColor.systemPink, range: r)
}
}For per-keystroke highlighting, scope the work to the edited paragraph and batch attribute changes inside beginEditing() / endEditing() regardless of which engine drives it.
string.count to build an NSRange, or using a UTF-16 length where a String.Index is expected — produces ranges that are off by however many emoji or combining marks the text contains. Bridge explicitly with NSRange(_, in:) or Range(_, in:) and never compute NSRange offsets from String.count.static let or stored on the owner. The same applies to Regex(string) runtime construction; literal regexes are compile-time and free.m.1 to be a Substring typed at compile time, use a literal or a RegexBuilder. AnyRegexOutput is fine for "did it match" or for indexing by name, costly for everything else..anchorsMatchLines. Swift Regex's equivalent is .anchorsMatchLineEndings() on the regex.setRenderingAttributes(_:for:) on NSTextLayoutManager rather than mutating the storage; the storage path triggers full editing-cycle work.processEditing sees the edited storage but invalidates ranges if the edit shifts characters after the matches. Either run the pass after endEditing() or restrict the pass to the edited paragraph./skill txt-detectors-tagger — NSDataDetector, NLTagger, NLTokenizer for tasks regex is wrong for/skill txt-markdown — Markdown rendering and AttributedString intent handling/skill txt-attributed-string — feeding regex output into AttributedString-based pipelines/skill txt-nstextstorage — applying matched attributes through NSTextStorage with proper editing batching~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.